Repository: rayokota/jsonata-python

Issue:
a `.` in $replace doesn't match reference implementation
Running a `$replace` in the js version of the library returns the full string:
<img width="277" alt="image" src="https://github.com/user-attachments/assets/3bda4c30-2e30-4e67-b3fb-b6ebe6774fcb">


But in `jsonata-python` returns an empty string
```
 echo '{}' | python3 -m jsonata.cli '$replace("hello", ".", "")'

```

This is due to the difference in `$replace` implementation in the `js` version of the library versus the `python` library. 

The `js` library does a simple replace:
<img width="730" alt="image" src="https://github.com/user-attachments/assets/9ec3fe39-0977-470c-a445-466e85bbcc43">

While the `python` library does a `re.sub` in this case:
<img width="927" alt="image" src="https://github.com/user-attachments/assets/cb8c1a17-0424-4384-b269-9c77736bbd8d">


Discussion:

Patch:
diff --git a/src/jsonata/functions.py b/src/jsonata/functions.py
index cdea849..fd69643 100644
--- a/src/jsonata/functions.py
+++ b/src/jsonata/functions.py
@@ -755,22 +755,82 @@ class Functions:
         if isinstance(pattern, str):
             if not pattern:
                 raise jexception.JException("Second argument of replace function cannot be an empty string", 0)
-        if limit is None:
-            if isinstance(pattern, str):
-                return re.sub(pattern, str(replacement), string)
-            else:
-                return Functions.safe_replace_all(string, pattern, replacement)
-        else:
+        
+        if limit is not None and limit < 0:
+            raise jexception.JException("Fourth argument of replace function must evaluate to a positive number", 0)
+
+        def string_replacer(match):
+            result = ''
+            position = 0
+            repl = str(replacement)
+            while position < len(repl):
+                index = repl.find('$', position)
+                if index == -1:
+                    result += repl[position:]
+                    break
+                result += repl[position:index]
+                position = index + 1
+                if position < len(repl):
+                    dollar_val = repl[position]
+                    if dollar_val == '$':
+                        result += '$'
+                        position += 1
+                    elif dollar_val == '0':
+                        result += match.group(0)
+                        position += 1
+                    else:
+                        max_digits = len(str(len(match.groups())))
+                        group_num = repl[position:position+max_digits]
+                        if group_num.isdigit():
+                            group_index = int(group_num)
+                            if 0 < group_index <= len(match.groups()):
+                                result += match.group(group_index) or ''
+                                position += len(group_num)
+                            else:
+                                result += '$'
+                        else:
+                            result += '$'
+                else:
+                    result += '$'
+            return result
 
-            if limit < 0:
-                raise jexception.JException("Fourth argument of replace function must evaluate to a positive number", 0)
+        if callable(replacement):
+            replacer = lambda m: replacement(m.groupdict())
+        elif isinstance(replacement, str):
+            replacer = string_replacer
+        else:
+            replacer = lambda m: str(replacement)
 
-            for i in range(0, limit):
-                if isinstance(pattern, str):
-                    string = re.sub(pattern, str(replacement), string, 1)
-                else:
-                    string = Functions.safe_replace_first(string, pattern, str(replacement))
-            return string
+        if isinstance(pattern, str):
+            # Use string methods for literal string patterns
+            result = ''
+            position = 0
+            count = 0
+            while True:
+                if limit is not None and count >= limit:
+                    result += string[position:]
+                    break
+                index = string.find(pattern, position)
+                if index == -1:
+                    result += string[position:]
+                    break
+                result += string[position:index]
+                match = re.match(re.escape(pattern), string[index:])
+                result += replacer(match)
+                position = index + len(pattern)
+                count += 1
+            return result
+        else:
+            # Use regex for pattern objects
+            if limit is None:
+                return Functions.safe_replace_all(string, pattern, replacement)
+            else:
+                count = 0
+                result = string
+                while count < limit:
+                    result = Functions.safe_replace_first(result, pattern, str(replacement))
+                    count += 1
+                return result
 
     #
     # Base64 encode a string


Test patch:
diff --git a/tests/string_test.py b/tests/string_test.py
index 4fcd09c..9b09b21 100644
--- a/tests/string_test.py
+++ b/tests/string_test.py
@@ -32,6 +32,12 @@ class TestString:
         assert jsonata.Jsonata("$string($)").evaluate({"a": str('\n')}) == "{\"a\":\"\\n\"}"
         assert jsonata.Jsonata("$string($)").evaluate({"a": "</"}) == "{\"a\":\"</\"}"
 
+    def test_replace(self):
+        assert jsonata.Jsonata("$replace('hello', '.', '')").evaluate(None) == "hello"
+        assert jsonata.Jsonata("$replace('hello', 'l', 'x')").evaluate(None) == "hexxo"
+        assert jsonata.Jsonata("$replace('h.ello', '.', '')").evaluate(None) == "hello"
+        assert jsonata.Jsonata("$replace('h.e.l.l.o', '.', '',2)").evaluate(None) == "hel.l.o"
+
     #
     # Additional $split tests
     #