diff --git a/.gitattributes b/.gitattributes
index 7057ad030..412eeda78 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -1,5 +1,5 @@
 # Auto detect text files and perform LF normalization
-* text=off
+* text=auto

 # Custom for Visual Studio
 *.cs     diff=csharp
diff --git a/extensions/jython/src/com/google/refine/jython/JythonEvaluable.java b/extensions/jython/src/com/google/refine/jython/JythonEvaluable.java
index e240320f4..bcccb4028 100644
--- a/extensions/jython/src/com/google/refine/jython/JythonEvaluable.java
+++ b/extensions/jython/src/com/google/refine/jython/JythonEvaluable.java
@@ -34,8 +34,10 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 package com.google.refine.jython;

 import java.io.File;
+import java.sql.Timestamp;
+import java.time.OffsetDateTime;
+import java.time.ZoneOffset;
 import java.util.ArrayList;
-import java.util.Iterator;
 import java.util.List;
 import java.util.Properties;

@@ -128,10 +130,19 @@ public class JythonEvaluable implements Evaluable {
     @Override
     public Object evaluate(Properties bindings) {
         try {
+            Object value = bindings.get("value");
+            PyObject pyValue;
+            if (value instanceof OffsetDateTime) {
+                // We know the OffsetDateTime is always at UTC, but just in case it ever changes, do the UTC change
+                pyValue = Py.newDatetime(Timestamp.valueOf(((OffsetDateTime) value).atZoneSameInstant(ZoneOffset.UTC).toLocalDateTime()));
+            } else {
+                pyValue = Py.java2py(value);
+            }
             // call the temporary PyFunction directly
             Object result = ((PyFunction) _engine.get(s_functionName)).__call__(
+
                     new PyObject[] {
-                            Py.java2py(bindings.get("value")),
+                            pyValue,
                             new JythonHasFieldsWrapper((HasFields) bindings.get("cell"), bindings),
                             new JythonHasFieldsWrapper((HasFields) bindings.get("cells"), bindings),
                             new JythonHasFieldsWrapper((HasFields) bindings.get("row"), bindings),
@@ -169,16 +180,23 @@ public class JythonEvaluable implements Evaluable {
     protected Object unwrap(PyObject po) {
         if (po instanceof PyNone) {
             return null;
+        } else if ("datetime".equals(po.getType().getName())) {
+            return OffsetDateTime.of(
+                    po.__getattr__("year").asInt(),
+                    po.__getattr__("month").asInt(),
+                    po.__getattr__("day").asInt(),
+                    po.__getattr__("hour").asInt(),
+                    po.__getattr__("minute").asInt(),
+                    po.__getattr__("second").asInt(),
+                    po.__getattr__("microsecond").asInt() * 1000, // scale to nanoseconds
+                    ZoneOffset.UTC);
         } else if (po.isNumberType()) {
             return po.asDouble();
         } else if (po.isSequenceType()) {
-            Iterator<PyObject> i = po.asIterable().iterator();
-
-            List<Object> list = new ArrayList<Object>();
-            while (i.hasNext()) {
-                list.add(unwrap((Object) i.next()));
+            List<Object> list = new ArrayList<>();
+            for (Object o : po.asIterable()) {
+                list.add(unwrap(o));
             }
-
             return list.toArray();
         } else {
             return po;

diff --git a/extensions/jython/tests/src/com/google/refine/jython/JythonEvaluableTest.java b/extensions/jython/tests/src/com/google/refine/jython/JythonEvaluableTest.java
index f97903336..733a232cd 100644
--- a/extensions/jython/tests/src/com/google/refine/jython/JythonEvaluableTest.java
+++ b/extensions/jython/tests/src/com/google/refine/jython/JythonEvaluableTest.java
@@ -1,6 +1,8 @@

 package com.google.refine.jython;

+import java.time.OffsetDateTime;
+import java.time.ZoneOffset;
 import java.util.Properties;

 import org.python.core.PyObject;
@@ -18,26 +20,29 @@ import com.google.refine.model.Row;
  */
 public class JythonEvaluableTest {

-    // Reproduces the situation when result is a PyObject
-    // Version with a test case which only calls the existing evaluate method
-    @Test
-    public void unwrapPyObjectTest() {
-        Properties props = new Properties();
+    private Properties createBindings() {
         Project project = new Project();
-
         Row row = new Row(2);
         row.setCell(0, new Cell("one", null));
         row.setCell(0, new Cell("1", null));
+        Properties bindings = new Properties();
+        bindings.put("columnName", "number");
+        bindings.put("true", "true");
+        bindings.put("false", "false");
+        bindings.put("rowIndex", "0");
+        bindings.put("value", 1);
+        bindings.put("project", project);
+        bindings.put("call", "number");
+        bindings.put("PI", "3.141592654");
+        bindings.put("cells", new CellTuple(project, row));
+        return bindings;
+    }

-        props.put("columnName", "number");
-        props.put("true", "true");
-        props.put("false", "false");
-        props.put("rowIndex", "0");
-        props.put("value", 1);
-        props.put("project", project);
-        props.put("call", "number");
-        props.put("PI", "3.141592654");
-        props.put("cells", new CellTuple(project, row));
+    // Reproduces the situation when result is a PyObject
+    // Version with a test case which only calls the existing evaluate method
+    @Test
+    public void unwrapPyObjectTest() {
+        Properties props = createBindings();
         String funcExpression = "class Foo(object):\n" +
                 "    bar = 1\n" +
                 "\n" +
@@ -49,22 +54,7 @@ public class JythonEvaluableTest {

     @Test
     public void testJythonConcurrent() {
-        Properties props = new Properties();
-        Project project = new Project();
-
-        Row row = new Row(2);
-        row.setCell(0, new Cell("one", null));
-        row.setCell(0, new Cell("1", null));
-
-        props.put("columnName", "number");
-        props.put("true", "true");
-        props.put("false", "false");
-        props.put("rowIndex", "0");
-        props.put("value", 1);
-        props.put("project", project);
-        props.put("call", "number");
-        props.put("PI", "3.141592654");
-        props.put("cells", new CellTuple(project, row));
+        Properties props = createBindings();

         Evaluable eval1 = new JythonEvaluable("a = value\nreturn a * 2");
         Long value1 = (Long) eval1.evaluate(props);
@@ -76,4 +66,71 @@ public class JythonEvaluableTest {
         Long value2 = (Long) eval1.evaluate(props);
         Assert.assertEquals(value1, value2);
     }
+
+    @Test
+    public void testJythonDate() {
+        Properties bindings = createBindings();
+        OffsetDateTime expectedDate = OffsetDateTime.of(2024, 10, 2, 12, 30, 30, 0, ZoneOffset.UTC);
+
+        // Test passing a date out from Python (wow, Python 2.7 timezone handling is primitive!)
+        Evaluable evaluable = new JythonEvaluable("from datetime import datetime,timedelta,tzinfo\n" +
+                "\n" +
+                "ZERO = timedelta(0)\n" +
+                "\n" +
+                "class UTC(tzinfo):\n" +
+                "    \"\"\"UTC\"\"\"\n" +
+                "\n" +
+                "    def utcoffset(self, dt):\n" +
+                "        return ZERO\n" +
+                "\n" +
+                "    def tzname(self, dt):\n" +
+                "        return \"UTC\"\n" +
+                "\n" +
+                "    def dst(self, dt):\n" +
+                "        return ZERO\n" +
+                "\n" +
+                "utc = UTC()" +
+                "\n" +
+                "return datetime(2024, 10, 2, 12, 30, 30, tzinfo=utc)");
+        Object result = evaluable.evaluate(bindings);
+        Assert.assertEquals(result, expectedDate);
+
+        // Test passing a date in
+        bindings.put("value", expectedDate);
+        evaluable = new JythonEvaluable("import datetime\n" +
+                "return isinstance(value,datetime.datetime)");
+        result = evaluable.evaluate(bindings);
+        Assert.assertEquals(result, 1L); // Python's version of true
+
+        evaluable = new JythonEvaluable("import datetime\n" +
+                "return str(value)");
+        result = evaluable.evaluate(bindings);
+        Assert.assertEquals(result, "2024-10-02 12:30:30");
+    }
+
+    @Test
+    public void testJythonDataTypes() {
+        Properties bindings = new Properties(); // tests are self-contained, so no bindings needed
+
+        // Test sequences
+        Evaluable evaluable = new JythonEvaluable("return (1,2)");
+        Object result = evaluable.evaluate(bindings);
+        Assert.assertEquals(result, new Long[] { 1L, 2L });
+
+        // Test floats
+        evaluable = new JythonEvaluable("return 1.2");
+        result = evaluable.evaluate(bindings);
+        Assert.assertEquals(result, 1.2);
+
+        // Test integers
+        evaluable = new JythonEvaluable("return 1");
+        result = evaluable.evaluate(bindings);
+        Assert.assertEquals(result, 1L);
+
+        // Test strings
+        evaluable = new JythonEvaluable("return 'foo'");
+        result = evaluable.evaluate(bindings);
+        Assert.assertEquals(result, "foo");
+
+    }
 }
diff --git a/main/tests/cypress/cypress/e2e/project/grid/column/facet/facets.numeric.cy.js b/main/tests/cypress/cypress/e2e/project/grid/column/facet/facets.numeric.cy.js
index 00d058978..839417216 100644
--- a/main/tests/cypress/cypress/e2e/project/grid/column/facet/facets.numeric.cy.js
+++ b/main/tests/cypress/cypress/e2e/project/grid/column/facet/facets.numeric.cy.js
@@ -38,23 +38,31 @@ describe(__filename, function () {
     cy.get('.slider-widget-bracket').eq(1)
     .trigger('mousedown',{ force: true })
     .trigger('mousemove',-130,0,{ force: true })
-    .trigger('mouseup',{ force: true })
+    .trigger('mouseup',{ force: true });
+    cy.get('.facet-range-status').contains('0 — 45');
     cy.get('#summary-bar').contains('72 matching rows');

-    //sliding the middle portion in numeric facet
+    // wait for the numeric facet to be refreshed before next mouse events
+    cy.get('.browsing-panel-indicator').should("not.be.visible");
+
+    // sliding entire selection by the body (middle)
     cy.get('.slider-widget-draggable').eq(0)
-      // FIXME: These are the last two uses of the hated waitForOrOperation
-    .trigger('mousedown',{ force: true }).waitForOrOperation()
+    .trigger('mousedown',{ force: true })
     .trigger('mousemove',130,0,{ force: true })
-    .trigger('mouseup',{ force: true }).waitForOrOperation();
-    cy.get('#summary-bar').contains('70 matching rows');
+    .trigger('mouseup',{ force: true });
+    cy.get('.facet-range-status').contains('23 — 68');
+    cy.get('#summary-bar').contains('74 matching rows');
+
+    // wait for the numeric facet to be refreshed before next mouse events
+    cy.get('.browsing-panel-indicator').should("not.be.visible");

     //sliding the left slider
     cy.get('.slider-widget-bracket').eq(0)
     .trigger('mousedown',{ force: true })
     .trigger('mousemove',130,0,{ force: true })
-    .trigger('mouseup',{ force: true })
-    cy.get('#summary-bar').contains('4 matching rows');
+    .trigger('mouseup',{ force: true });
+    cy.get('.facet-range-status').contains('66 — 68');
+    cy.get('#summary-bar').contains('3 matching rows');
   });

   it('Test for checkboxes and reset button', function () {
diff --git a/main/tests/cypress/cypress/support/commands.js b/main/tests/cypress/cypress/support/commands.js
index 306132747..72ccf187a 100644
--- a/main/tests/cypress/cypress/support/commands.js
+++ b/main/tests/cypress/cypress/support/commands.js
@@ -189,21 +189,6 @@ Cypress.Commands.add('navigateTo', (target) => {
   cy.get('#action-area-tabs li').contains(target).click();
 });

-/**
- * Wait for OpenRefine to finish an Ajax load
- *
- * @deprecated
- *
- * NOTE: This command is unreliable because if you call it after starting an operation e.g. with a click(), the loading
- * indicator may have come and gone already by the time waitForOrOperation() is called, causing the cypress test to
- * wait forever on ajax_in_progress=true until it fails due to timeout.
- *
- */
-Cypress.Commands.add('waitForOrOperation', () => {
-  cy.get('body[ajax_in_progress="true"]');
-  cy.get('body[ajax_in_progress="false"]');
-});
-
 /**
  * Utility method to fill something into the expression input
  */

Fixes #692

Changes proposed in this pull request:
- adds support for passing dates into and out of Jython
- includes tests for passing dates both directions
- simplifies sequence unwrapping
- add tests for sequences, ints, floats, strings

Add date handling to Jython.  Fixes #692

Add date handling to Jython.  Fixes #692


diff --git a/.gitignore b/.gitignore
index 08f43909a..459392486 100644
--- a/.gitignore
+++ b/.gitignore
@@ -69,6 +69,10 @@ node_modules/
 # Ignore the node_modules folder as they are dependencies to be installed by end-user
 main/webapp/node_modules

+# Yarn internal files
+**/.yarn
+**/.yarnrc.yml
+
 # Ignore .gitignore among extensions as it can be used to ignore custom extensions

 extensions/.gitignore
diff --git a/extensions/wikibase/module/scripts/dialogs/schema-list-item.html b/extensions/wikibase/module/scripts/dialogs/schema-list-item.html
index 8ce783b1e..334f625b5 100644
--- a/extensions/wikibase/module/scripts/dialogs/schema-list-item.html
+++ b/extensions/wikibase/module/scripts/dialogs/schema-list-item.html
@@ -2,7 +2,7 @@

-

+

          |
          |

diff --git a/extensions/wikibase/module/styles/dialogs/schema-management-dialog.css b/extensions/wikibase/module/styles/dialogs/schema-management-dialog.css
index 01f4618e0..890e3fec2 100644
--- a/extensions/wikibase/module/styles/dialogs/schema-management-dialog.css
+++ b/extensions/wikibase/module/styles/dialogs/schema-management-dialog.css
@@ -1,5 +1,6 @@
 .schema-template-list {
-  list-style: none;
+  list-style: decimal;
+  list-style-position: inside;
   max-height: 450px;
   overflow-y: auto;
 }
@@ -10,8 +11,7 @@

 .schema-template-list li {
   padding: .5em;
-  display: flex;
-  justify-content: space-between;
+  display: list-item;
 }

 .schema-template-list li:nth-child(odd) {
diff --git a/main/src/com/google/refine/expr/functions/Length.java b/main/src/com/google/refine/expr/functions/Length.java
index d7fde5db9..6ca73c740 100644
--- a/main/src/com/google/refine/expr/functions/Length.java
+++ b/main/src/com/google/refine/expr/functions/Length.java
@@ -37,6 +37,8 @@ import java.util.Collection;
 import java.util.Properties;

 import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+
 import com.google.refine.expr.EvalError;
 import com.google.refine.expr.HasFieldsList;
 import com.google.refine.grel.ControlFunctionRegistry;
@@ -61,6 +63,8 @@ public class Length implements Function {
                     return ((HasFieldsList) v).length();
                 } else if (v instanceof ArrayNode) {
                     return ((ArrayNode) v).size();
+                } else if (v instanceof ObjectNode) {
+                    return ((ObjectNode) v).size();
                 } else {
                     String s = (v instanceof String ? (String) v : v.toString());
                     return s.length();
diff --git a/main/src/com/google/refine/model/Row.java b/main/src/com/google/refine/model/Row.java
index b1d2f295f..9eaf291a1 100644
--- a/main/src/com/google/refine/model/Row.java
+++ b/main/src/com/google/refine/model/Row.java
@@ -80,7 +80,7 @@ public class Row implements HasFields {

     /**
      * Copy a row and return the copy. Note that this is a shallow copy, so if the contents of cells are changed in the
-     * original, they will be be changed in the duplicate.
+     * original, they will be changed in the duplicate.
      *
      * @return the duplicated row
      */
diff --git a/main/src/com/google/refine/model/changes/MassRowChange.java b/main/src/com/google/refine/model/changes/MassRowChange.java
index 860495485..d79ac1f0e 100644
--- a/main/src/com/google/refine/model/changes/MassRowChange.java
+++ b/main/src/com/google/refine/model/changes/MassRowChange.java
@@ -46,6 +46,8 @@ import com.google.refine.model.Project;
 import com.google.refine.model.Row;
 import com.google.refine.util.Pool;

+// TODO: This replaces all rows in the project with a new set of rows, but if only a small percentage of the rows
+// are being changed, it'd be much more efficient to apply a change list of new/modified rows.
 public class MassRowChange implements Change {

     final protected List<Row> _newRows;
diff --git a/main/src/com/google/refine/operations/cell/MultiValuedCellSplitOperation.java b/main/src/com/google/refine/operations/cell/MultiValuedCellSplitOperation.java
index da0d93cb3..f80d9bec8 100644
--- a/main/src/com/google/refine/operations/cell/MultiValuedCellSplitOperation.java
+++ b/main/src/com/google/refine/operations/cell/MultiValuedCellSplitOperation.java
@@ -1,6 +1,6 @@
 /*

-Copyright 2010, Google Inc.
+Copyright 2010, 2023 Google Inc. & OpenRefine contributors
 All rights reserved.

 Redistribution and use in source and binary forms, with or without
@@ -59,6 +59,7 @@ public class MultiValuedCellSplitOperation extends AbstractOperation {
     final protected String _mode;
     final protected String _separator;
     final protected Boolean _regex;
+    private final Pattern _pattern;

     final protected int[] _fieldLengths;

@@ -94,6 +95,11 @@ public class MultiValuedCellSplitOperation extends AbstractOperation {
         _separator = separator;
         _mode = "separator";
         _regex = regex;
+        if (_regex) {
+            _pattern = Pattern.compile(_separator, Pattern.UNICODE_CHARACTER_CLASS);
+        } else {
+            _pattern = null;
+        }

         _fieldLengths = null;
     }
@@ -108,7 +114,14 @@ public class MultiValuedCellSplitOperation extends AbstractOperation {
         _mode = "lengths";
         _separator = null;
         _regex = null;
+        _pattern = null;

+        // Make sure all of our lengths are non-negative
+        for (int i = 0; i < fieldLengths.length; i++) {
+            if (fieldLengths[i] < 0) {
+                fieldLengths[i] = 0;
+            }
+        }
         _fieldLengths = fieldLengths;
     }

@@ -176,45 +189,39 @@ public class MultiValuedCellSplitOperation extends AbstractOperation {

             Object value = oldRow.getCellValue(cellIndex);
             String s = value instanceof String ? ((String) value) : value.toString();
-            String[] values = null;
+            String[] values;
             if ("lengths".equals(_mode)) {
                 if (_fieldLengths.length > 0 && _fieldLengths[0] > 0) {
                     values = new String[_fieldLengths.length];

-                    int lastIndex = 0;
-
+                    int from = 0;
+                    int end = s.length();
                     for (int i = 0; i < _fieldLengths.length; i++) {
-                        int thisIndex = lastIndex;
-
-                        Object o = _fieldLengths[i];
-                        if (o instanceof Number) {
-                            thisIndex = Math.min(s.length(), lastIndex + Math.max(0, ((Number) o).intValue()));
-                        }
-
-                        values[i] = s.substring(lastIndex, thisIndex);
-                        lastIndex = thisIndex;
+                        int to = Math.min(end, from + _fieldLengths[i]);
+                        values[i] = s.substring(from, to);
+                        from = to;
                     }
+                } else {
+                    values = new String[] { s };
                 }
             } else if (_regex) {
-                Pattern pattern = Pattern.compile(_separator, Pattern.UNICODE_CHARACTER_CLASS);
-                values = pattern.split(s);
+                values = _pattern.split(s);
             } else {
                 values = StringUtils.splitByWholeSeparatorPreserveAllTokens(s, _separator);
             }

+            // Split didn't change anything. Just copy the row
             if (values.length < 2) {
                 newRows.add(oldRow.dup());
                 continue;
             }

-            // First value goes into the same row
-            {
-                Row firstNewRow = oldRow.dup();
-                firstNewRow.setCell(cellIndex, new Cell(values[0], null));
-
-                newRows.add(firstNewRow);
-            }
+            // First newly split value goes into the original cell in the existing row
+            Row firstNewRow = oldRow.dup();
+            firstNewRow.setCell(cellIndex, new Cell(values[0], null));
+            newRows.add(firstNewRow);

+            // For remaining values, use an empty cell, if one exists in the row, otherwise allocate a new row
             int r2 = r + 1;
             for (int v = 1; v < values.length; v++) {
                 Cell newCell = new Cell(values[v], null);
@@ -222,6 +229,7 @@ public class MultiValuedCellSplitOperation extends AbstractOperation {
                 if (r2 < project.rows.size()) {
                     Row oldRow2 = project.rows.get(r2);
                     if (oldRow2.isCellBlank(cellIndex) &&
+                    // key cell not blank means we are on next record
                             oldRow2.isCellBlank(keyCellIndex)) {

                         Row newRow = oldRow2.dup();
@@ -234,6 +242,7 @@ public class MultiValuedCellSplitOperation extends AbstractOperation {
                     }
                 }

+                // We need a new (empty) row
                 Row newRow = new Row(cellIndex + 1);
                 newRow.setCell(cellIndex, newCell);

diff --git a/main/webapp/modules/core/scripts/views/data-table/menu-edit-cells.js b/main/webapp/modules/core/scripts/views/data-table/menu-edit-cells.js
index f2c2625ce..087b0922a 100644
--- a/main/webapp/modules/core/scripts/views/data-table/menu-edit-cells.js
+++ b/main/webapp/modules/core/scripts/views/data-table/menu-edit-cells.js
@@ -362,14 +362,14 @@ DataTableColumnHeaderUI.extendMenu(function(column, columnHeaderUI, menu) {
           return;
         }
       } else if (mode === "cases") {
-        if(elmts.reversTranistionCases[0].checked) {
+        if(elmts.reverseTransitionCases[0].checked) {
           config.separator = "(?<=\\p{Upper}|[\\p{Upper}][\\s])(?=\\p{Lower})";
         } else {
           config.separator = "(?<=\\p{Lower}|[\\p{Lower}][\\s])(?=\\p{Upper})";
         }
         config.regex = true;
       } else if (mode === "number") {
-        if(elmts.reversTranistionNumbers[0].checked) {
+        if(elmts.reverseTransitionNumbers[0].checked) {
           config.separator = "(?<=\\p{L}|[\\p{L}][\\s])(?=\\p{Digit})";
         } else {
           config.separator = "(?<=\\p{Digit}|[\\p{Digit}][\\s])(?=\\p{L})";
diff --git a/main/webapp/modules/core/scripts/views/data-table/split-multi-valued-cells-dialog.html b/main/webapp/modules/core/scripts/views/data-table/split-multi-valued-cells-dialog.html
index 72942f18a..f544d3bd5 100644
--- a/main/webapp/modules/core/scripts/views/data-table/split-multi-valued-cells-dialog.html
+++ b/main/webapp/modules/core/scripts/views/data-table/split-multi-valued-cells-dialog.html
@@ -31,7 +31,7 @@

               <input type="radio" name="split-by-mode" value="cases" id="$split-multi-valued-cells-by-case" />

-              <input type="checkbox" bind="reversTranistionCases" id="$split-cell-transition-order-case" />
+              <input type="checkbox" bind="reverseTransitionCases" id="$split-cell-transition-order-case" />

@@ -42,7 +42,7 @@

               <input type="radio" name="split-by-mode" value="number" id="$split-multi-valued-cells-by-number" />

-              <input type="checkbox" bind="reversTranistionNumbers" id="$split-cell-transition-order-number" />
+              <input type="checkbox" bind="reverseTransitionNumbers" id="$split-cell-transition-order-number" />

@@ -59,4 +59,4 @@
       <button class="button" bind="cancelButton"></button>

-

\ No newline at end of file
+

diff --git a/packaging/pom.xml b/packaging/pom.xml
index a07206e70..27c94caba 100644
--- a/packaging/pom.xml
+++ b/packaging/pom.xml
@@ -574,7 +574,7 @@
                    https://stackoverflow.com/questions/53696439/appbundle-maven-plugin-fails-on-mac-osx-with-java-10 -->
               <groupId>org.codehaus.plexus</groupId>
               <artifactId>plexus-archiver</artifactId>
-              <version>4.7.1</version>
+              <version>4.8.0</version>
             </dependency>
             <dependency>
               <groupId>org.apache.velocity</groupId>
diff --git a/pom.xml b/pom.xml
index 7522c027c..4f5ef1c2d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -81,7 +81,7 @@
     <okhttp.version>4.10.0</okhttp.version>
     <jena.version>4.8.0</jena.version>
     <poi.version>5.2.3</poi.version>
-    <commons-lang3.version>3.12.0</commons-lang3.version>
+    <commons-lang3.version>3.13.0</commons-lang3.version>
     <commons-fileupload.version>1.5</commons-fileupload.version>
     <commons-collections.version>3.2.2</commons-collections.version>
     <commons-text.version>1.10.0</commons-text.version>

diff --git a/main/tests/cypress/package.json b/main/tests/cypress/package.json
index c9b25e828..e480e5008 100644
--- a/main/tests/cypress/package.json
+++ b/main/tests/cypress/package.json
@@ -16,13 +16,13 @@
         "lint": "prettier --check . && eslint ."
     },
     "dependencies": {
-        "cypress": "12.17.1",
+        "cypress": "12.17.2",
         "cypress-file-upload": "^5.0.8"
     },
     "devDependencies": {
-        "eslint": "^8.45.0",
+        "eslint": "^8.46.0",
         "eslint-config-google": "^0.14.0",
-        "eslint-config-prettier": "^8.8.0",
+        "eslint-config-prettier": "^8.9.0",
         "eslint-plugin-cypress": "^2.13.3",
         "prettier": "3.0.0"
     }
diff --git a/main/tests/cypress/yarn.lock b/main/tests/cypress/yarn.lock
index 066e8d958..135b33a3e 100644
--- a/main/tests/cypress/yarn.lock
+++ b/main/tests/cypress/yarn.lock
@@ -46,15 +46,15 @@
   dependencies:
     eslint-visitor-keys "^3.3.0"

-"@eslint-community/regexpp@^4.4.0":
-  version "4.4.0"
-  resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.4.0.tgz#3e61c564fcd6b921cb789838631c5ee44df09403"
-  integrity sha512-A9983Q0LnDGdLPjxyXQ00sbV+K+O+ko2Dr+CZigbHWtX9pNfxlaBkMR8X1CztI73zuEyEBXTVjx7CE+/VSwDiQ==
+"@eslint-community/regexpp@^4.6.1":
+  version "4.6.2"
+  resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.6.2.tgz#1816b5f6948029c5eaacb0703b850ee0cb37d8f8"
+  integrity sha512-pPTNuaAG3QMH+buKyBIGJs3g/S5y0caxw0ygM3YyE6yJFySwiGGSzA+mM3KJ8QQvzeLh3blwgSonkFjgQdxzMw==

-"@eslint/eslintrc@^2.1.0":
-  version "2.1.0"
-  resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.0.tgz#82256f164cc9e0b59669efc19d57f8092706841d"
-  integrity sha512-Lj7DECXqIVCqnqjjHMPna4vn6GJcMgul/wuS0je9OZ9gsL0zzDpKPVtcG1HaDVc+9y+qgXneTeUMbCqXJNpH1A==
+"@eslint/eslintrc@^2.1.1":
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.1.tgz#18d635e24ad35f7276e8a49d135c7d3ca6a46f93"
+  integrity sha512-9t7ZA7NGGK8ckelF0PQCfcxIUzs1Md5rrO6U/c+FIQNanea5UZC0wqKXH4vHBccmu4ZJgZ2idtPeW7+Q2npOEA==
   dependencies:
     ajv "^6.12.4"
     debug "^4.3.2"
@@ -66,10 +66,10 @@
     minimatch "^3.1.2"
     strip-json-comments "^3.1.1"

-"@eslint/js@8.44.0":
-  version "8.44.0"
-  resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.44.0.tgz#961a5903c74139390478bdc808bcde3fc45ab7af"
-  integrity sha512-Ag+9YM4ocKQx9AarydN0KY2j0ErMHNIocPDrVo8zAE44xLTjEtz81OdR68/cydGtk6m6jDb5Za3r2useMzYmSw==
+"@eslint/js@^8.46.0":
+  version "8.46.0"
+  resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.46.0.tgz#3f7802972e8b6fe3f88ed1aabc74ec596c456db6"
+  integrity sha512-a8TLtmPi8xzPkCbp/OGFUo5yhRkHM2Ko9kOWP4znJr0WAhWyThaw3PnwX4vOTWOAMsV2uRt32PPDcEz63esSaA==

 "@humanwhocodes/config-array@^0.11.10":
   version "0.11.10"
@@ -161,7 +161,7 @@ aggregate-error@^3.0.0:
     clean-stack "^2.0.0"
     indent-string "^4.0.0"

-ajv@^6.10.0, ajv@^6.12.4:
+ajv@^6.12.4:
   version "6.12.6"
   resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz"
   integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==
@@ -436,10 +436,10 @@ cypress-file-upload@^5.0.8:
   resolved "https://registry.yarnpkg.com/cypress-file-upload/-/cypress-file-upload-5.0.8.tgz#d8824cbeaab798e44be8009769f9a6c9daa1b4a1"
   integrity sha512-+8VzNabRk3zG6x8f8BWArF/xA/W0VK4IZNx3MV0jFWrJS/qKn8eHfa5nU73P9fOQAgwHFJx7zjg4lwOnljMO8g==

-cypress@12.17.1:
-  version "12.17.1"
-  resolved "https://registry.yarnpkg.com/cypress/-/cypress-12.17.1.tgz#777fdcceec4ecd642fc90795f5994853b6ea03f8"
-  integrity sha512-eKfBgO6t8waEyhegL4gxD7tcI6uTCGttu+ZU7y9Hq8BlpMztd7iLeIF4AJFAnbZH1xjX+wwgg4cRKFNSvv3VWQ==
+cypress@12.17.2:
+  version "12.17.2"
+  resolved "https://registry.yarnpkg.com/cypress/-/cypress-12.17.2.tgz#040ac55de1e811f6e037d231a2869d5ab8c29c85"
+  integrity sha512-hxWAaWbqQBzzMuadSGSuQg5PDvIGOovm6xm0hIfpCVcORsCAj/gF2p0EvfnJ4f+jK2PCiDgP6D2eeE9/FK4Mjg==
   dependencies:
     "@cypress/request" "^2.88.11"
     "@cypress/xvfb" "^1.2.4"
@@ -569,10 +569,10 @@ eslint-config-google@^0.14.0:
   resolved "https://registry.yarnpkg.com/eslint-config-google/-/eslint-config-google-0.14.0.tgz"
   integrity sha512-WsbX4WbjuMvTdeVL6+J3rK1RGhCTqjsFjX7UMSMgZiyxxaNLkoJENbrGExzERFeoTpGw3F3FypTiWAP9ZXzkEw==

-eslint-config-prettier@^8.8.0:
-  version "8.8.0"
-  resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.8.0.tgz#bfda738d412adc917fd7b038857110efe98c9348"
-  integrity sha512-wLbQiFre3tdGgpDv67NQKnJuTlcUVYHas3k+DZCc2U2BadthoEY4B7hLPvAxaqdyOGCzuLfii2fqGph10va7oA==
+eslint-config-prettier@^8.9.0:
+  version "8.9.0"
+  resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.9.0.tgz#094b6254b2804b0544f7cee535f802b6d29ee10b"
+  integrity sha512-+sbni7NfVXnOpnRadUA8S28AUlsZt9GjgFvABIRL9Hkn8KqNzOp+7Lw4QWtrwn20KzU3wqu1QoOj2m+7rKRqkA==

 eslint-plugin-cypress@^2.13.3:
   version "2.13.3"
@@ -581,40 +581,40 @@ eslint-plugin-cypress@^2.13.3:
   dependencies:
     globals "^11.12.0"

-eslint-scope@^7.2.0:
-  version "7.2.0"
-  resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.0.tgz#f21ebdafda02352f103634b96dd47d9f81ca117b"
-  integrity sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==
+eslint-scope@^7.2.2:
+  version "7.2.2"
+  resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.2.tgz#deb4f92563390f32006894af62a22dba1c46423f"
+  integrity sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==
   dependencies:
     esrecurse "^4.3.0"
     estraverse "^5.2.0"

-eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1:
-  version "3.4.1"
-  resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz#c22c48f48942d08ca824cc526211ae400478a994"
-  integrity sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA==
+eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.2:
+  version "3.4.2"
+  resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.2.tgz#8c2095440eca8c933bedcadf16fefa44dbe9ba5f"
+  integrity sha512-8drBzUEyZ2llkpCA67iYrgEssKDUu68V8ChqqOfFupIaG/LCVPUT+CoGJpT77zJprs4T/W7p07LP7zAIMuweVw==

-eslint@^8.45.0:
-  version "8.45.0"
-  resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.45.0.tgz#bab660f90d18e1364352c0a6b7c6db8edb458b78"
-  integrity sha512-pd8KSxiQpdYRfYa9Wufvdoct3ZPQQuVuU5O6scNgMuOMYuxvH0IGaYK0wUFjo4UYYQQCUndlXiMbnxopwvvTiw==
+eslint@^8.46.0:
+  version "8.46.0"
+  resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.46.0.tgz#a06a0ff6974e53e643acc42d1dcf2e7f797b3552"
+  integrity sha512-cIO74PvbW0qU8e0mIvk5IV3ToWdCq5FYG6gWPHHkx6gNdjlbAYvtfHmlCMXxjcoVaIdwy/IAt3+mDkZkfvb2Dg==
   dependencies:
     "@eslint-community/eslint-utils" "^4.2.0"
-    "@eslint-community/regexpp" "^4.4.0"
-    "@eslint/eslintrc" "^2.1.0"
-    "@eslint/js" "8.44.0"
+    "@eslint-community/regexpp" "^4.6.1"
+    "@eslint/eslintrc" "^2.1.1"
+    "@eslint/js" "^8.46.0"
     "@humanwhocodes/config-array" "^0.11.10"
     "@humanwhocodes/module-importer" "^1.0.1"
     "@nodelib/fs.walk" "^1.2.8"
-    ajv "^6.10.0"
+    ajv "^6.12.4"
     chalk "^4.0.0"
     cross-spawn "^7.0.2"
     debug "^4.3.2"
     doctrine "^3.0.0"
     escape-string-regexp "^4.0.0"
-    eslint-scope "^7.2.0"
-    eslint-visitor-keys "^3.4.1"
-    espree "^9.6.0"
+    eslint-scope "^7.2.2"
+    eslint-visitor-keys "^3.4.2"
+    espree "^9.6.1"
     esquery "^1.4.2"
     esutils "^2.0.2"
     fast-deep-equal "^3.1.3"
@@ -637,10 +637,10 @@ eslint@^8.45.0:
     strip-ansi "^6.0.1"
     text-table "^0.2.0"

-espree@^9.6.0:
-  version "9.6.0"
-  resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.0.tgz#80869754b1c6560f32e3b6929194a3fe07c5b82f"
-  integrity sha512-1FH/IiruXZ84tpUlm0aCUEwMl2Ho5ilqVh0VvQXw+byAz/4SAciyHLlfmL5WYqsvD38oymdUwBss0LtK8m4s/A==
+espree@^9.6.0, espree@^9.6.1:
+  version "9.6.1"
+  resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f"
+  integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==
   dependencies:
     acorn "^8.9.0"
     acorn-jsx "^5.3.2"
diff --git a/main/tests/server/src/com/google/refine/expr/functions/LengthTests.java b/main/tests/server/src/com/google/refine/expr/functions/LengthTests.java
index 3e04d14d6..14bc957bd 100644
--- a/main/tests/server/src/com/google/refine/expr/functions/LengthTests.java
+++ b/main/tests/server/src/com/google/refine/expr/functions/LengthTests.java
@@ -27,9 +27,35 @@

 package com.google.refine.expr.functions;

+import static org.testng.Assert.assertEquals;
+
+import java.util.Properties;
+
 import org.testng.annotations.Test;

-import com.google.refine.util.TestUtils;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+
+import com.google.refine.RefineTest;
+import com.google.refine.grel.Function;
+import com.google.refine.util.ParsingUtilities;
+
+public class LengthTests extends RefineTest {
+
+    static Properties bindings = new Properties();
+
+    @Test
+    public void testLength() throws JsonProcessingException {
+        Function f = new Length();
+        assertEquals(f.call(bindings, new Object[] { Long.valueOf(123) }), 3);
+        assertEquals(f.call(bindings, new Object[] { Double.valueOf(123.1) }), 5);
+        assertEquals(f.call(bindings, new Object[] { "12345" }), 5);
+
+        ArrayNode array = (ArrayNode) ParsingUtilities.mapper.readTree("[1,2,3]");
+        assertEquals(f.call(bindings, new Object[] { array }), 3);

-public class LengthTests {
+        ObjectNode dict = (ObjectNode) ParsingUtilities.mapper.readTree("{\"a\": \"b\", \"c\": \"d\"}");
+        assertEquals(f.call(bindings, new Object[] { dict }), 2);
+    }
 }

Fixes #5893

Changes proposed in this pull request:
- JavaScript code was edited to append a number to the schema name.
- The change should be purely visual. The schema name variables are not edited.

Add numbering to the list of Wikibase schemas

def parser(log: str) -> dict[str, str]:
    import re
    import xml.etree.ElementTree as ET

    results: dict[str, str] = {}

    # Helper: merge statuses with precedence fail > skip > pass
    def merge_status(name: str, new_status: str):
        if new_status not in ("pass", "fail", "skip"):
            return
        old_status = results.get(name)
        if old_status is None:
            results[name] = new_status
            return
        if old_status == new_status:
            return
        # Precedence: fail > skip > pass
        priority = {"fail": 3, "skip": 2, "pass": 1}
        if priority[new_status] > priority[old_status]:
            results[name] = new_status

    # -----------------------
    # XML-based parsing pass
    # -----------------------

    # Non-greedy patterns for JUnit/TestNG XML blocks
    xml_pattern = re.compile(
        r"(<testng-results[\s\S]*?</testng-results>|<testsuite[\s\S]*?</testsuite>)",
        re.IGNORECASE,
    )

    for match in xml_pattern.finditer(log):
        xml_block = match.group(1)
        try:
            root = ET.fromstring(xml_block)
        except ET.ParseError:
            # Skip this block but continue with others
            continue

        tag_lower = root.tag.lower()

        # Handle TestNG summary reports (<testng-results>)
        if tag_lower.endswith("testng-results"):
            # Look for all test-method elements anywhere
            for tm in root.findall(".//test-method"):
                # Skip configuration methods
                if tm.attrib.get("is-config", "false").lower() == "true":
                    continue
                name = tm.attrib.get("name", "")
                if not name:
                    continue
                # Try to get class name from surrounding <class> element
                class_elem = tm.find("..")
                class_name = ""
                # xml.etree doesn't support parent lookup directly, so search ancestors:
                # We instead search for the nearest <class> ancestor manually:
            # Since parent lookup is tricky, fall back to the attribute 'class' on <test-method> if present
            # or to a surrounding <class> element found via xpath with name match, which is usually enough.
            # Re-parse with a simpler loop using suites/tests/classes for better class name resolution.
            for suite in root.findall(".//suite"):
                for test in suite.findall(".//test"):
                    for clazz in test.findall(".//class"):
                        class_name = clazz.attrib.get("name", "")
                        for tm in clazz.findall("test-method"):
                            if tm.attrib.get("is-config", "false").lower() == "true":
                                continue
                            m_name = tm.attrib.get("name", "")
                            if not m_name:
                                continue
                            fq_name = f"{class_name}.{m_name}" if class_name else m_name
                            status_attr = tm.attrib.get("status", "").upper()
                            # Map TestNG status to our statuses
                            if status_attr == "PASS":
                                status = "pass"
                            elif status_attr in ("FAIL", "FAILURE"):
                                status = "fail"
                            elif status_attr == "SKIP":
                                status = "skip"
                            else:
                                # Fallback: if there is an exception element, mark as fail; else pass
                                if tm.find(".//exception") is not None:
                                    status = "fail"
                                else:
                                    status = "pass"
                            merge_status(fq_name, status)
            continue

        # Handle JUnit-style <testsuite> reports
        if tag_lower.endswith("testsuite"):
            for tc in root.findall(".//testcase"):
                class_name = tc.attrib.get("classname", "")
                name = tc.attrib.get("name", "")
                if not name:
                    continue
                fq_name = f"{class_name}.{name}" if class_name else name

                # Determine status via children
                status = "pass"
                if tc.find("skipped") is not None:
                    status = "skip"
                elif tc.find("failure") is not None or tc.find("error") is not None:
                    status = "fail"

                merge_status(fq_name, status)

    # ---------------------------
    # Text-based parsing fallback
    # ---------------------------

    # Compile regexes for common frameworks

    # pytest: path::test_name STATUS
    pytest_re = re.compile(
        r"^(?P<name>\S+::\S+)\s+"
        r"(?P<status>PASSED|FAILED|SKIPPED|ERROR|XFAILED|XPASSED)\b"
    )

    # unittest / nose: ClassName.test_method ... ok/FAIL/ERROR/skipped
    unittest_re = re.compile(
        r"^(?P<class>[A-Za-z_]\w*)\.(?P<method>[A-Za-z_]\w*)\s+\.\.\.\s+"
        r"(?P<status>ok|FAIL|ERROR|skipped)\b"
    )

    # go test: --- STATUS: TestName
    gotest_re = re.compile(
        r"^---\s+(?P<status>PASS|FAIL|SKIP):\s+(?P<name>\S+)"
    )

    # Jest-like symbol lines: ✓/✕/○ test name
    jest_symbol_re = re.compile(r"^\s*([✓✕○])\s+(?P<name>.+)")

    # Map various raw statuses to our canonical ones
    def map_status(raw: str) -> str | None:
        r = raw.upper()
        if r in ("PASSED", "OK", "XPASSED", "PASS"):
            return "pass"
        if r in ("FAILED", "FAIL", "ERROR", "XFAILED"):
            return "fail"
        if r in ("SKIPPED", "SKIP", "SKIPPED (EXPECTED FAILURE)"):
            return "skip"
        return None

    for line in log.splitlines():
        line = line.rstrip("\n")

        # pytest
        m = pytest_re.match(line)
        if m:
            name = m.group("name")
            status = map_status(m.group("status"))
            if status:
                merge_status(name, status)
            continue

        # unittest
        m = unittest_re.match(line)
        if m:
            name = f"{m.group('class')}.{m.group('method')}"
            status = map_status(m.group("status"))
            if status:
                merge_status(name, status)
            continue

        # go test
        m = gotest_re.match(line)
        if m:
            name = m.group("name")
            status = map_status(m.group("status"))
            if status:
                merge_status(name, status)
            continue

        # Jest symbols
        m = jest_symbol_re.match(line)
        if m:
            symbol = m.group(1)
            name = m.group("name").strip()
            if symbol == "✓":
                status = "pass"
            elif symbol == "✕":
                status = "fail"
            elif symbol == "○":
                status = "skip"
            else:
                status = None
            if status:
                merge_status(name, status)
            continue

    return results

export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
apt-get install -y -qq nodejs npm
./refine build

find . -maxdepth 6 -path '*surefire-reports/*.xml' -print0 | xargs -0 -I {} sh -c 'echo "===== {} ====="; cat {}'


diff --git a/extensions/wikibase/src/org/openrefine/wikibase/qa/EditInspector.java b/extensions/wikibase/src/org/openrefine/wikibase/qa/EditInspector.java
index 6567a5844..6256e54b6 100644
--- a/extensions/wikibase/src/org/openrefine/wikibase/qa/EditInspector.java
+++ b/extensions/wikibase/src/org/openrefine/wikibase/qa/EditInspector.java
@@ -68,6 +68,7 @@ import org.openrefine.wikibase.qa.scrutinizers.UnsourcedScrutinizer;
 import org.openrefine.wikibase.qa.scrutinizers.UseAsQualifierScrutinizer;
 import org.openrefine.wikibase.qa.scrutinizers.WhitespaceScrutinizer;
 import org.openrefine.wikibase.schema.WikibaseSchema;
+import org.openrefine.wikibase.schema.entityvalues.ReconEntityIdValue;
 import org.openrefine.wikibase.schema.entityvalues.SuggestedEntityIdValue;
 import org.openrefine.wikibase.schema.entityvalues.SuggestedItemIdValue;
 import org.openrefine.wikibase.schema.entityvalues.SuggestedPropertyIdValue;
@@ -217,17 +218,23 @@ public class EditInspector {
         }
     }

-    private void resolveWarningPropertyLabels() throws ExecutionException {
+    protected void resolveWarningPropertyLabels() throws ExecutionException {
         if (entityCache != null) {
             List<EntityIdValue> propertyIdValues = warningStore.getWarnings().stream()
                     .flatMap(warning -> warning.getProperties().entrySet().stream()
-                            .filter(entry -> entry.getValue() instanceof EntityIdValue)
                             .filter(entry -> {
-                                if (entry instanceof SuggestedEntityIdValue) {
-                                    String label = ((SuggestedEntityIdValue) entry.getValue()).getLabel();
+                                Object value = entry.getValue();
+                                if (!(value instanceof PropertyIdValue || value instanceof ItemIdValue)) {
+                                    return false;
+                                }
+                                if (value instanceof SuggestedEntityIdValue) {
+                                    String label = ((SuggestedEntityIdValue) value).getLabel();
                                     return label == null || label.isEmpty();
+                                } else if (value instanceof ReconEntityIdValue) {
+                                    return false; // already contains labels via the recon object
+                                } else {
+                                    return true; // no label associated with the value, so we fetch it
                                 }
-                                return true;
                             })
                             .map(entry -> (EntityIdValue) entry.getValue()))
                     .collect(Collectors.toList());
@@ -237,7 +244,7 @@ public class EditInspector {
             warningStore.getWarnings().stream()
                     .forEach(warning -> {
                         warning.getProperties().forEach((key, value) -> {
-                            if (value instanceof PropertyIdValue || value instanceof ItemIdValue) {
+                            if (value instanceof EntityIdValue && propertyIdValues.contains(value)) {
                                 EntityIdValue entityIdValue = (EntityIdValue) value;
                                 String label = "";

diff --git a/extensions/wikibase/tests/src/org/openrefine/wikibase/qa/EditInspectorTest.java b/extensions/wikibase/tests/src/org/openrefine/wikibase/qa/EditInspectorTest.java
index b952607fa..2d8fd1ac3 100644
--- a/extensions/wikibase/tests/src/org/openrefine/wikibase/qa/EditInspectorTest.java
+++ b/extensions/wikibase/tests/src/org/openrefine/wikibase/qa/EditInspectorTest.java
@@ -1,12 +1,17 @@

 package org.openrefine.wikibase.qa;

+import static org.mockito.Mockito.mock;
 import static org.testng.Assert.assertEquals;

 import org.testng.annotations.Test;

+import com.google.refine.model.Recon;
+
 import org.openrefine.wikibase.manifests.Manifest;
 import org.openrefine.wikibase.manifests.ManifestParser;
+import org.openrefine.wikibase.qa.QAWarning.Severity;
+import org.openrefine.wikibase.schema.entityvalues.ReconItemIdValue;
 import org.openrefine.wikibase.testing.TestingData;

 public class EditInspectorTest {
@@ -37,4 +42,24 @@ public class EditInspectorTest {
         EditInspector editInspector = new EditInspector(new QAWarningStore(), manifest, false);
         assertEquals(editInspector.scrutinizers.size(), scrutinizerNotDependingOnPropertyConstraintCount);
     }
+
+    @Test
+    public void testLabelFetchingIgnoresReconciledEntityIds() throws Exception {
+        // regression test for https://github.com/OpenRefine/OpenRefine/issues/7089
+        String manifestJson = TestingData.jsonFromFile("manifest/wikidata-manifest-v1.0.json");
+        Manifest manifest = ManifestParser.parse(manifestJson);
+        QAWarningStore qaWarningStore = new QAWarningStore();
+        QAWarning qaWarning1 = new QAWarning("my-warning", "my-warning", Severity.WARNING, 1);
+        Recon recon = mock(Recon.class);
+        recon.judgment = Recon.Judgment.New;
+        ReconItemIdValue reconItemIdValue = new ReconItemIdValue(recon, "cell value");
+        qaWarning1.setProperty("example_entity", reconItemIdValue);
+        qaWarning1.setProperty("some_string", "foo");
+        qaWarningStore.addWarning(qaWarning1);
+
+        EditInspector editInspector = new EditInspector(qaWarningStore, manifest, false);
+        editInspector.resolveWarningPropertyLabels();
+
+        assertEquals(qaWarning1.getProperties().get("example_entity"), reconItemIdValue);
+    }
 }

Closes #7089.

This clarifies the filtering logic to select which entity ids should be fetched to get their labels, to avoid fetching them for entity ids generated via reconciliation. Those already have associated labels (existing label for existing entities, cell values for new entities).

wikibase: Fix label fetching in QA issues for new entities


diff --git a/src/main/java/com/thealgorithms/dynamicprogramming/Tribonacci.java b/src/main/java/com/thealgorithms/dynamicprogramming/Tribonacci.java
new file mode 100644
index 00000000..99f90290
--- /dev/null
+++ b/src/main/java/com/thealgorithms/dynamicprogramming/Tribonacci.java
@@ -0,0 +1,30 @@
+package com.thealgorithms.dynamicprogramming;
+
+/**
+ * The {@code Tribonacci} class provides a method to compute the n-th number in the Tribonacci sequence.
+ * N-th Tribonacci Number - https://leetcode.com/problems/n-th-tribonacci-number/description/
+ */
+public class Tribonacci {
+
+    /**
+     * Computes the n-th Tribonacci number.
+     *
+     * @param n the index of the Tribonacci number to compute
+     * @return the n-th Tribonacci number
+     */
+    public static int compute(int n) {
+        if (n == 0) return 0;
+        if (n == 1 || n == 2) return 1;
+
+        int first = 0, second = 1, third = 1;
+
+        for (int i = 3; i <= n; i++) {
+            int next = first + second + third;
+            first = second;
+            second = third;
+            third = next;
+        }
+
+        return third;
+    }
+}

diff --git a/src/test/java/com/thealgorithms/dynamicprogramming/TribonacciTest.java b/src/test/java/com/thealgorithms/dynamicprogramming/TribonacciTest.java
new file mode 100644
index 00000000..434a1825
--- /dev/null
+++ b/src/test/java/com/thealgorithms/dynamicprogramming/TribonacciTest.java
@@ -0,0 +1,24 @@
+package com.thealgorithms.dynamicprogramming;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.junit.jupiter.api.Test;
+
+/**
+ * Test class for {@code Tribonacci}.
+ */
+public class TribonacciTest {
+
+    /**
+     * Tests the Tribonacci computation for a set of known values.
+     */
+    @Test
+    public void testKnownValues() {
+        assertEquals(0, Tribonacci.compute(0), "The 0th Tribonacci should be 0.");
+        assertEquals(1, Tribonacci.compute(1), "The 1st Tribonacci should be 1.");
+        assertEquals(1, Tribonacci.compute(2), "The 2nd Tribonacci should be 1.");
+        assertEquals(2, Tribonacci.compute(3), "The 3rd Tribonacci should be 2.");
+        assertEquals(4, Tribonacci.compute(4), "The 4th Tribonacci should be 4.");
+        assertEquals(7, Tribonacci.compute(5), "The 5th Tribonacci should be 7.");
+    }
+}

This PR introduces the `Tribonacci` class to the `dynamicprogramming` package. It implements an efficient method to compute the N-th number in the Tribonacci sequence as defined by the problem statement on LeetCode: https://leetcode.com/problems/n-th-tribonacci-number/description/.

Resolves #4646

## Changes Made

- Added `Tribonacci.java` with a static `compute` method.
- Included tests in `TribonacciTest.java`.

The `compute` method calculates the N-th Tribonacci number using an iterative approach with O(n) time complexity and O(1) space complexity, which is optimal for the constraints given.

<!--
Thank you for your contribution!
In order to reduce the number of notifications sent to the maintainers, please:
- create your PR as draft, cf. https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests#draft-pull-requests,
- make sure that all of the CI checks pass,
- mark your PR as ready for review, cf. https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/changing-the-stage-of-a-pull-request#marking-a-pull-request-as-ready-for-review
-->

<!-- For completed items, change [ ] to [x] -->

- [x] I have read [CONTRIBUTING.md](https://github.com/TheAlgorithms/Java/blob/master/CONTRIBUTING.md).
- [x] This pull request is all my own work -- I have not plagiarized it.
- [x] All filenames are in PascalCase.
- [x] All functions and variable names follow Java naming conventions.
- [x] All new algorithms have a URL in their comments that points to Wikipedia or other similar explanations.
- [x] All new code is formatted with `clang-format -i --style=file path/to/your/file.java`

def parse_java_mvn(log: str) -> dict[str, str]:
    """Parse Maven logs into {identifier: status}."""
    results = {}

    failure_re = re.compile(
        r"^\[ERROR\]\s+(?P<class>[A-Za-z0-9_$.]+)\.(?P<method>[A-Za-z0-9_$.]+?)(?::(?P<line>\d+))?\b"
    )
    running_re = re.compile(r"^(?:\[INFO\]\s+)?Running\s+([A-Za-z0-9_$.]+)$")
    summary_re = re.compile(
        r"Tests run:\s*(\d+),\s*Failures:\s*(\d+),\s*Errors:\s*(\d+),\s*Skipped:\s*(\d+)"
    )

    seen_running = set()
    current_running = None

    for raw in log.splitlines():
        line = raw.strip()
        if not line:
            continue
        rm = running_re.match(line)
        if rm:
            running_name = rm.group(1)
            current_running = running_name
            seen_running.add(running_name)
            continue
        fm = failure_re.match(line)
        if fm:
            clazz = fm.group("class")
            method = fm.group("method")
            test_id = f"{clazz}.{method}"
            results[test_id] = "FAILED"
            results.setdefault(clazz, "FAILED")
            continue

        sm = summary_re.search(line)
        if sm:
            _tests_run, failures, errors, skipped = map(int, sm.groups())
            if failures > 0 or errors > 0:
                status = "FAILED"
            elif skipped > 0:
                status = "SKIPPED"
            else:
                status = "PASSED"
            if current_running:
                results.setdefault(current_running, status)
            current_running = None

    if not results and seen_running:
        for clazz in seen_running:
            results.setdefault(clazz, "PASSED")
    return results

bash -c '{ mvn --batch-mode test ; } > /tmp/test.log 2>&1'


diff --git a/main/src/com/google/refine/expr/functions/arrays/Join.java b/main/src/com/google/refine/expr/functions/arrays/Join.java
index e40e25e0f..917684407 100644
--- a/main/src/com/google/refine/expr/functions/arrays/Join.java
+++ b/main/src/com/google/refine/expr/functions/arrays/Join.java
@@ -76,15 +76,18 @@ public class Join implements Function {
                         int l = a.size();

                         for (int i = 0; i < l; i++) {
-                            if (!isFirst) {
-                                sb.append(separator);
+                            Object o = JsonValueConverter.convert(a.get(i));
+                            if (o != null) {
+                                if (!isFirst) {
+                                    sb.append(separator);
+                                }
+                                sb.append(o).toString();
+                                isFirst = false;
                             }
-                            // FIXME: This throws NPE for arrays containing nulls
-                            sb.append(JsonValueConverter.convert(a.get(i)).toString());
-                            isFirst = false;
                         }
                     } else {
                         for (Object o : ExpressionUtils.toObjectList(v)) {
+                            // TODO: Another instance of null being treated differently than empty string
                             if (o != null) {
                                 if (!isFirst) {
                                     sb.append(separator);

diff --git a/main/tests/server/src/com/google/refine/expr/functions/arrays/JoinTests.java b/main/tests/server/src/com/google/refine/expr/functions/arrays/JoinTests.java
index 8453cf7da..08ca4ddae 100644
--- a/main/tests/server/src/com/google/refine/expr/functions/arrays/JoinTests.java
+++ b/main/tests/server/src/com/google/refine/expr/functions/arrays/JoinTests.java
@@ -59,9 +59,9 @@ public class JoinTests extends GrelTestBase {
         parseEval(bindings, new String[] { "['','',''].join('|')", "||" });

         // Tests for JSON array
-        // Both of the following tests throw NPEs. First is desired behavior, second is current array behavior
+        // First test below is desired behavior, second is current array behavior
 //        parseEval(bindings, new String[] { "'[null,null,null]'.parseJson().join('|')", "||" } );
-//        parseEval(bindings, new String[] { "'[null,null,null]'.parseJson().join('|')", "" } );
+        parseEval(bindings, new String[] { "'[null,null,null]'.parseJson().join('|')", "" });
         parseEval(bindings, new String[] { "'[\"\",2,3]'.parseJson().join('|')", "|2|3" });
         parseEval(bindings, new String[] { "'[1,2,\"\"]'.parseJson().join('|')", "1|2|" });
         parseEval(bindings, new String[] { "'[\"\",\"\",\"\"]'.parseJson().join('|')", "||" });

Fixes #6683

Changes proposed in this pull request:
- Fix join() of arrays containing nulls

Fix join() of arrays containing nulls. Fixes #6683


diff --git a/src/main/java/com/thealgorithms/misc/MedianOfRunningArray.java b/src/main/java/com/thealgorithms/misc/MedianOfRunningArray.java
index 61fbfa82..62013ee3 100644
--- a/src/main/java/com/thealgorithms/misc/MedianOfRunningArray.java
+++ b/src/main/java/com/thealgorithms/misc/MedianOfRunningArray.java
@@ -6,10 +6,10 @@ import java.util.PriorityQueue;
 /**
  * @author shrutisheoran
  */
-public class MedianOfRunningArray {
+public abstract class MedianOfRunningArray<T extends Number & Comparable<T>> {

-    private PriorityQueue<Integer> maxHeap;
-    private PriorityQueue<Integer> minHeap;
+    private PriorityQueue<T> maxHeap;
+    private PriorityQueue<T> minHeap;

     // Constructor
     public MedianOfRunningArray() {
@@ -21,8 +21,8 @@ public class MedianOfRunningArray {
       Inserting lower half of array to max Heap
       and upper half to min heap
      */
-    public void insert(Integer e) {
-        if (!minHeap.isEmpty() && e < minHeap.peek()) {
+    public void insert(final T e) {
+        if (!minHeap.isEmpty() && e.compareTo(minHeap.peek()) < 0) {
             maxHeap.offer(e);
             if (maxHeap.size() > minHeap.size() + 1) {
                 minHeap.offer(maxHeap.poll());
@@ -38,14 +38,16 @@ public class MedianOfRunningArray {
     /*
       Returns median at any given point
      */
-    public double median() {
+    public T median() {
         if (maxHeap.isEmpty() && minHeap.isEmpty()) {
             throw new IllegalArgumentException("Enter at least 1 element, Median of empty list is not defined!");
+        } else if (maxHeap.size() == minHeap.size()) {
+            T maxHeapTop = maxHeap.peek();
+            T minHeapTop = minHeap.peek();
+            return calculateAverage(maxHeapTop, minHeapTop);
         }
-
-        if (maxHeap.size() == minHeap.size()) {
-            return (maxHeap.peek() + minHeap.peek()) / 2.0;
-        }
-        return maxHeap.size() > minHeap.size() ? maxHeap.peek() * 1.0 : minHeap.peek() * 1.0;
+        return maxHeap.size() > minHeap.size() ? maxHeap.peek() : minHeap.peek();
     }
+
+    public abstract T calculateAverage(T a, T b);
 }
diff --git a/src/main/java/com/thealgorithms/misc/MedianOfRunningArrayByte.java b/src/main/java/com/thealgorithms/misc/MedianOfRunningArrayByte.java
new file mode 100644
index 00000000..668f651e
--- /dev/null
+++ b/src/main/java/com/thealgorithms/misc/MedianOfRunningArrayByte.java
@@ -0,0 +1,8 @@
+package com.thealgorithms.misc;
+
+public final class MedianOfRunningArrayByte extends MedianOfRunningArray<Byte> {
+    @Override
+    public Byte calculateAverage(final Byte a, final Byte b) {
+        return (byte) ((a + b) / 2);
+    }
+}
diff --git a/src/main/java/com/thealgorithms/misc/MedianOfRunningArrayDouble.java b/src/main/java/com/thealgorithms/misc/MedianOfRunningArrayDouble.java
new file mode 100644
index 00000000..9d743de5
--- /dev/null
+++ b/src/main/java/com/thealgorithms/misc/MedianOfRunningArrayDouble.java
@@ -0,0 +1,8 @@
+package com.thealgorithms.misc;
+
+public final class MedianOfRunningArrayDouble extends MedianOfRunningArray<Double> {
+    @Override
+    public Double calculateAverage(final Double a, final Double b) {
+        return (a + b) / 2.0d;
+    }
+}
diff --git a/src/main/java/com/thealgorithms/misc/MedianOfRunningArrayFloat.java b/src/main/java/com/thealgorithms/misc/MedianOfRunningArrayFloat.java
new file mode 100644
index 00000000..a667abf6
--- /dev/null
+++ b/src/main/java/com/thealgorithms/misc/MedianOfRunningArrayFloat.java
@@ -0,0 +1,8 @@
+package com.thealgorithms.misc;
+
+public final class MedianOfRunningArrayFloat extends MedianOfRunningArray<Float> {
+    @Override
+    public Float calculateAverage(final Float a, final Float b) {
+        return (a + b) / 2.0f;
+    }
+}
diff --git a/src/main/java/com/thealgorithms/misc/MedianOfRunningArrayInteger.java b/src/main/java/com/thealgorithms/misc/MedianOfRunningArrayInteger.java
new file mode 100644
index 00000000..7154ba07
--- /dev/null
+++ b/src/main/java/com/thealgorithms/misc/MedianOfRunningArrayInteger.java
@@ -0,0 +1,8 @@
+package com.thealgorithms.misc;
+
+public final class MedianOfRunningArrayInteger extends MedianOfRunningArray<Integer> {
+    @Override
+    public Integer calculateAverage(final Integer a, final Integer b) {
+        return (a + b) / 2;
+    }
+}
diff --git a/src/main/java/com/thealgorithms/misc/MedianOfRunningArrayLong.java b/src/main/java/com/thealgorithms/misc/MedianOfRunningArrayLong.java
new file mode 100644
index 00000000..1f138c63
--- /dev/null
+++ b/src/main/java/com/thealgorithms/misc/MedianOfRunningArrayLong.java
@@ -0,0 +1,8 @@
+package com.thealgorithms.misc;
+
+public final class MedianOfRunningArrayLong extends MedianOfRunningArray<Long> {
+    @Override
+    public Long calculateAverage(final Long a, final Long b) {
+        return (a + b) / 2L;
+    }
+}

diff --git a/src/test/java/com/thealgorithms/misc/MedianOfRunningArrayTest.java b/src/test/java/com/thealgorithms/misc/MedianOfRunningArrayTest.java
index 96cdc77e..90910d51 100644
--- a/src/test/java/com/thealgorithms/misc/MedianOfRunningArrayTest.java
+++ b/src/test/java/com/thealgorithms/misc/MedianOfRunningArrayTest.java
@@ -1,15 +1,13 @@
 package com.thealgorithms.misc;

 import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertThrows;

-import org.apache.commons.lang3.tuple.Pair;
 import org.junit.jupiter.api.Test;

 /**
- * Test case for Two sum Problem.
- * @author Bama Charan Chhandogi (https://github.com/BamaCharanChhandogi)
+ * Test case for Median Of Running Array Problem.
+ * @author Ansh Shah (https://github.com/govardhanshah456)
  */

 public class MedianOfRunningArrayTest {
@@ -17,71 +15,71 @@ public class MedianOfRunningArrayTest {

     @Test
     public void testWhenInvalidInoutProvidedShouldThrowException() {
-        MedianOfRunningArray stream = new MedianOfRunningArray();
+        var stream = new MedianOfRunningArrayInteger();
         IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> stream.median());
         assertEquals(exception.getMessage(), EXCEPTION_MESSAGE);
     }

     @Test
     public void testWithNegativeValues() {
-        MedianOfRunningArray stream = new MedianOfRunningArray();
+        var stream = new MedianOfRunningArrayInteger();
         stream.insert(-1);
         assertEquals(-1, stream.median());
         stream.insert(-2);
-        assertEquals(-1.5, stream.median());
+        assertEquals(-1, stream.median());
         stream.insert(-3);
         assertEquals(-2, stream.median());
     }

     @Test
     public void testWithSingleValues() {
-        MedianOfRunningArray stream = new MedianOfRunningArray();
+        var stream = new MedianOfRunningArrayInteger();
         stream.insert(-1);
         assertEquals(-1, stream.median());
     }

     @Test
     public void testWithRandomValues() {
-        MedianOfRunningArray stream = new MedianOfRunningArray();
+        var stream = new MedianOfRunningArrayInteger();
         stream.insert(10);
-        assertEquals(10.0, stream.median());
+        assertEquals(10, stream.median());

         stream.insert(5);
-        assertEquals(7.5, stream.median());
+        assertEquals(7, stream.median());

         stream.insert(20);
-        assertEquals(10.0, stream.median());
+        assertEquals(10, stream.median());

         stream.insert(15);
-        assertEquals(12.5, stream.median());
+        assertEquals(12, stream.median());

         stream.insert(25);
-        assertEquals(15.0, stream.median());
+        assertEquals(15, stream.median());

         stream.insert(30);
-        assertEquals(17.5, stream.median());
+        assertEquals(17, stream.median());

         stream.insert(35);
-        assertEquals(20.0, stream.median());
+        assertEquals(20, stream.median());

         stream.insert(1);
-        assertEquals(17.5, stream.median());
+        assertEquals(17, stream.median());
     }

     @Test
     public void testWithNegativeAndPositiveValues() {
-        MedianOfRunningArray stream = new MedianOfRunningArray();
+        var stream = new MedianOfRunningArrayInteger();
         stream.insert(-1);
         assertEquals(-1, stream.median());
         stream.insert(2);
-        assertEquals(0.5, stream.median());
+        assertEquals(0, stream.median());
         stream.insert(-3);
         assertEquals(-1, stream.median());
     }

     @Test
     public void testWithDuplicateValues() {
-        MedianOfRunningArray stream = new MedianOfRunningArray();
+        var stream = new MedianOfRunningArrayInteger();
         stream.insert(-1);
         assertEquals(-1, stream.median());
         stream.insert(-1);
@@ -92,7 +90,7 @@ public class MedianOfRunningArrayTest {

     @Test
     public void testWithDuplicateValuesB() {
-        MedianOfRunningArray stream = new MedianOfRunningArray();
+        var stream = new MedianOfRunningArrayInteger();
         stream.insert(10);
         stream.insert(20);
         stream.insert(10);
@@ -105,7 +103,7 @@ public class MedianOfRunningArrayTest {

     @Test
     public void testWithLargeValues() {
-        MedianOfRunningArray stream = new MedianOfRunningArray();
+        var stream = new MedianOfRunningArrayInteger();
         stream.insert(1000000);
         assertEquals(1000000, stream.median());
         stream.insert(12000);
@@ -113,49 +111,89 @@ public class MedianOfRunningArrayTest {
         stream.insert(15000000);
         assertEquals(1000000, stream.median());
         stream.insert(2300000);
-        assertEquals(1650000.00, stream.median());
+        assertEquals(1650000, stream.median());
     }

     @Test
     public void testWithLargeCountOfValues() {
-        MedianOfRunningArray stream = new MedianOfRunningArray();
+        var stream = new MedianOfRunningArrayInteger();
         for (int i = 1; i <= 1000; i++) stream.insert(i);
-        assertEquals(500.5, stream.median());
+        assertEquals(500, stream.median());
     }

     @Test
     public void testWithThreeValuesInDescendingOrder() {
-        MedianOfRunningArray stream = new MedianOfRunningArray();
+        var stream = new MedianOfRunningArrayInteger();
         stream.insert(30);
         stream.insert(20);
         stream.insert(10);
-        assertEquals(20.0, stream.median());
+        assertEquals(20, stream.median());
     }

     @Test
     public void testWithThreeValuesInOrder() {
-        MedianOfRunningArray stream = new MedianOfRunningArray();
+        var stream = new MedianOfRunningArrayInteger();
         stream.insert(10);
         stream.insert(20);
         stream.insert(30);
-        assertEquals(20.0, stream.median());
+        assertEquals(20, stream.median());
     }

     @Test
     public void testWithThreeValuesNotInOrderA() {
-        MedianOfRunningArray stream = new MedianOfRunningArray();
+        var stream = new MedianOfRunningArrayInteger();
         stream.insert(30);
         stream.insert(10);
         stream.insert(20);
-        assertEquals(20.0, stream.median());
+        assertEquals(20, stream.median());
     }

     @Test
     public void testWithThreeValuesNotInOrderB() {
-        MedianOfRunningArray stream = new MedianOfRunningArray();
+        var stream = new MedianOfRunningArrayInteger();
         stream.insert(20);
         stream.insert(10);
         stream.insert(30);
-        assertEquals(20.0, stream.median());
+        assertEquals(20, stream.median());
+    }
+
+    @Test
+    public void testWithFloatValues() {
+        var stream = new MedianOfRunningArrayFloat();
+        stream.insert(20.0f);
+        assertEquals(20.0f, stream.median());
+        stream.insert(10.5f);
+        assertEquals(15.25f, stream.median());
+        stream.insert(30.0f);
+        assertEquals(20.0f, stream.median());
+    }
+
+    @Test
+    public void testWithByteValues() {
+        var stream = new MedianOfRunningArrayByte();
+        stream.insert((byte) 120);
+        assertEquals((byte) 120, stream.median());
+        stream.insert((byte) -120);
+        assertEquals((byte) 0, stream.median());
+        stream.insert((byte) 127);
+        assertEquals((byte) 120, stream.median());
+    }
+
+    @Test
+    public void testWithLongValues() {
+        var stream = new MedianOfRunningArrayLong();
+        stream.insert(120000000L);
+        assertEquals(120000000L, stream.median());
+        stream.insert(92233720368547757L);
+        assertEquals(46116860244273878L, stream.median());
+    }
+
+    @Test
+    public void testWithDoubleValues() {
+        var stream = new MedianOfRunningArrayDouble();
+        stream.insert(12345.67891);
+        assertEquals(12345.67891, stream.median());
+        stream.insert(23456789.98);
+        assertEquals(Double.valueOf(11734567.83), stream.median(), .01);
     }
 }