From a0cef61c98b58d75d4ce3c1123e707a4dc62fffb Mon Sep 17 00:00:00 2001 From: Skip Breidbach Date: Mon, 28 Sep 2026 14:52:17 -0700 Subject: [PATCH 1/2] fix TypeErrors in $substring --- src/jsonata/functions.py | 25 +++++++++++++++++++------ tests/string_test.py | 14 ++++++++++++++ 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/src/jsonata/functions.py b/src/jsonata/functions.py index fa158a0..b54d029 100644 --- a/src/jsonata/functions.py +++ b/src/jsonata/functions.py @@ -230,7 +230,14 @@ def substring(string: Optional[str], start: Optional[float], length: Optional[fl if string is utils.Utils.NULL_VALUE: raise jexception.JException("T0410", -1) - start = int(start) if start is not None else None + if start is utils.Utils.NULL_VALUE or length is utils.Utils.NULL_VALUE: + raise jexception.JException("T0410", -1) + + # match JS: undefined start returns the whole string, or "" if a length is given + if start is None: + return string if length is None else "" + + start = int(start) length = int(length) if length is not None else None # not used: var strArray = stringToArray(string) @@ -338,6 +345,9 @@ def substring_after(string: Optional[str], chars: Optional[str]) -> Optional[str if string is utils.Utils.NULL_VALUE: raise jexception.JException("T0410", -1) + if chars is None: + return string + pos = string.find(chars) if pos > -1: return string[pos + len(chars):] @@ -442,12 +452,15 @@ def pad(string: Optional[str], width: Optional[int], char: Optional[str]) -> Opt if char is None or not char: char = " " + # match JS: undefined width adds no padding + if width is None: + return string + # match JS: truncate width to integer - if width is not None: - try: - width = int(width) - except Exception: - width = 0 + try: + width = int(width) + except Exception: + width = 0 if width < 0: result = Functions.left_pad(string, -width, char) diff --git a/tests/string_test.py b/tests/string_test.py index aa4028a..7578c03 100644 --- a/tests/string_test.py +++ b/tests/string_test.py @@ -1,4 +1,5 @@ import jsonata +import pytest # @@ -145,3 +146,16 @@ def test_trim(self): assert jsonata.Jsonata("$trim(\" \")").evaluate(None) == "" assert jsonata.Jsonata("$trim(\"\")").evaluate(None) == "" assert jsonata.Jsonata("$trim(notthere)").evaluate(None) is None + + def test_undefined_args(self): + assert jsonata.Jsonata("$substring('abc', $nothing)").evaluate(None) == "abc" + assert jsonata.Jsonata("$substring('abc', $nothing, 1)").evaluate(None) == "" + assert jsonata.Jsonata("$substring(?, $nothing)('abc')").evaluate(None) == "abc" + assert jsonata.Jsonata("$substringAfter('abc', $nothing)").evaluate(None) == "abc" + assert jsonata.Jsonata("$pad('abc', $nothing)").evaluate(None) == "abc" + + def test_substring_null_args(self): + for expr in ["$substring('abc', null)", "$substring('abc', 1, null)", "$substring('abc', $nothing, null)"]: + with pytest.raises(jsonata.JException) as exc_info: + jsonata.Jsonata(expr).evaluate(None) + assert exc_info.value.error == "T0410" From 8cd37ed781307d003b1ecaf09a9997590a97d7dd Mon Sep 17 00:00:00 2001 From: Skip Breidbach Date: Tue, 29 Sep 2026 10:40:57 -0700 Subject: [PATCH 2/2] reordered null checks; applied to each substring method as well as pad --- src/jsonata/functions.py | 27 ++++++++++++--------------- tests/string_test.py | 8 ++++++-- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/src/jsonata/functions.py b/src/jsonata/functions.py index b54d029..15cb759 100644 --- a/src/jsonata/functions.py +++ b/src/jsonata/functions.py @@ -223,16 +223,13 @@ def validate_input(arg: Optional[Any]) -> None: # @staticmethod def substring(string: Optional[str], start: Optional[float], length: Optional[float]) -> Optional[str]: + if string is utils.Utils.NULL_VALUE or start is utils.Utils.NULL_VALUE or length is utils.Utils.NULL_VALUE: + raise jexception.JException("T0410", -1) + # undefined inputs always return undefined if string is None: return None - if string is utils.Utils.NULL_VALUE: - raise jexception.JException("T0410", -1) - - if start is utils.Utils.NULL_VALUE or length is utils.Utils.NULL_VALUE: - raise jexception.JException("T0410", -1) - # match JS: undefined start returns the whole string, or "" if a length is given if start is None: return string if length is None else "" @@ -314,13 +311,13 @@ def substr(string: str, start: int, length: int) -> str: # @staticmethod def substring_before(string: Optional[str], chars: Optional[str]) -> Optional[str]: + if string is utils.Utils.NULL_VALUE or chars is utils.Utils.NULL_VALUE: + raise jexception.JException("T0410", -1) + # undefined inputs always return undefined if string is None: return None - if string is utils.Utils.NULL_VALUE: - raise jexception.JException("T0410", -1) - if chars is None: return string @@ -338,13 +335,13 @@ def substring_before(string: Optional[str], chars: Optional[str]) -> Optional[st # @staticmethod def substring_after(string: Optional[str], chars: Optional[str]) -> Optional[str]: + if string is utils.Utils.NULL_VALUE or chars is utils.Utils.NULL_VALUE: + raise jexception.JException("T0410", -1) + # undefined inputs always return undefined if string is None: return None - if string is utils.Utils.NULL_VALUE: - raise jexception.JException("T0410", -1) - if chars is None: return string @@ -442,13 +439,13 @@ def trim(string: Optional[str]) -> Optional[str]: # @staticmethod def pad(string: Optional[str], width: Optional[int], char: Optional[str]) -> Optional[str]: + if string is utils.Utils.NULL_VALUE or width is utils.Utils.NULL_VALUE or char is utils.Utils.NULL_VALUE: + raise jexception.JException("T0410", -1) + # undefined inputs always return undefined if string is None: return None - if string is utils.Utils.NULL_VALUE: - raise jexception.JException("T0410", -1) - if char is None or not char: char = " " diff --git a/tests/string_test.py b/tests/string_test.py index 7578c03..5df6a91 100644 --- a/tests/string_test.py +++ b/tests/string_test.py @@ -154,8 +154,12 @@ def test_undefined_args(self): assert jsonata.Jsonata("$substringAfter('abc', $nothing)").evaluate(None) == "abc" assert jsonata.Jsonata("$pad('abc', $nothing)").evaluate(None) == "abc" - def test_substring_null_args(self): - for expr in ["$substring('abc', null)", "$substring('abc', 1, null)", "$substring('abc', $nothing, null)"]: + def test_null_args(self): + for expr in ["$substring('abc', null)", "$substring('abc', 1, null)", "$substring('abc', $nothing, null)", + "$substring($nothing, null)", "$substring($nothing, 1, null)", + "$pad('abc', null)", "$pad('abc', 5, null)", "$pad($nothing, null)", + "$substringBefore('abc', null)", "$substringBefore($nothing, null)", + "$substringAfter('abc', null)", "$substringAfter($nothing, null)"]: with pytest.raises(jsonata.JException) as exc_info: jsonata.Jsonata(expr).evaluate(None) assert exc_info.value.error == "T0410"