Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 7 additions & 11 deletions src/jsonata/functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import decimal
import functools
import inspect
import itertools
import json
import math
import random
Expand All @@ -36,7 +37,7 @@
import unicodedata
import urllib.parse
from dataclasses import dataclass
from typing import Any, AnyStr, Mapping, NoReturn, Optional, Sequence, Callable, Type, Union
from typing import Any, AnyStr, Mapping, NoReturn, Optional, Sequence, Callable, Iterator, Type, Union

from jsonata import datetimeutils, jexception, parser, utils
from jsonata.regex_engine import CompiledPattern
Expand Down Expand Up @@ -515,8 +516,7 @@ class RegexpMatch:
# @returns {object} - structure that represents the match(es)
#
@staticmethod
def evaluate_matcher(matcher: CompiledPattern, string: Optional[str]) -> list[RegexpMatch]:
res = []
def evaluate_matcher(matcher: CompiledPattern, string: Optional[str]) -> Iterator[RegexpMatch]:
matches = matcher.finditer(string)
for m in matches:
groups = []
Expand All @@ -528,8 +528,7 @@ def evaluate_matcher(matcher: CompiledPattern, string: Optional[str]) -> list[Re

rm = Functions.RegexpMatch(m.group(), m.start(), groups)
rm.groups = groups
res.append(rm)
return res
yield rm

#
# Tests if the str contains the token
Expand All @@ -555,7 +554,7 @@ def contains(string: Optional[str], token: Union[None, str, CompiledPattern]) ->
# if (dbg) System.out.println("match = "+matches)
# result = (typeof matches !== 'undefined')
# throw new Error("regexp not impl"); //result = false
result = bool(matches)
result = next(matches, None) is not None
else:
raise RuntimeError("unknown type to match: " + str(token))

Expand Down Expand Up @@ -583,16 +582,13 @@ def match_(string: Optional[str], regex: Optional[CompiledPattern], limit: Optio

result = utils.Utils.create_sequence()
matches = Functions.evaluate_matcher(regex, string)
max = sys.maxsize
if limit is not None:
max = limit
matches = itertools.islice(matches, math.ceil(limit))

for i, rm in enumerate(matches):
for rm in matches:
m = {"match": rm.match, "index": rm.index, "groups": rm.groups}
# Convert to JSON map:
result.append(m)
if i >= max:
break
return result

#
Expand Down
18 changes: 5 additions & 13 deletions src/jsonata/jsonata.py
Original file line number Diff line number Diff line change
Expand Up @@ -1643,8 +1643,7 @@ def partial_apply_procedure(self, proc: Optional[parser.Parser.Symbol], args: Se
for param in proc.arguments:
# proc.arguments.forEach(Object (param, index) {
arg = args[index] if index < len(args) else None
if (arg is None) or (
isinstance(arg, parser.Parser.Symbol) and ("operator" == arg.type and "?" == arg.value)):
if isinstance(arg, parser.Parser.Symbol) and ("operator" == arg.type and "?" == arg.value):
unbound_args.append(param)
else:
env.bind(str(param.value), arg)
Expand All @@ -1671,17 +1670,10 @@ def partial_apply_native_function(self, native: Optional[JFunction], args: Seque

# var body = "function($a,$c) { $substring($a,0,$c) }"

sig_args = []
part_args = []
i = 0
while i < native.get_number_of_args():
arg_name = "$" + chr(ord('a') + i)
sig_args.append(arg_name)
if i >= len(args) or args[i] is None:
part_args.append(arg_name)
else:
part_args.append(args[i])
i += 1
# leave out trailing args that weren't supplied, so the native is called
# without them and optional params aren't validated against undefined
nargs = min(len(args), native.get_number_of_args())
sig_args = ["$" + chr(ord('a') + i) for i in range(nargs)]

body = "function(" + ", ".join(sig_args) + "){"
body += "$" + native.function_name + "(" + ", ".join(sig_args) + ") }"
Expand Down
60 changes: 60 additions & 0 deletions tests/string_test.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
锘縤mport jsonata
from jsonata.regex_engine import default_regex_engine


#
Expand Down Expand Up @@ -145,3 +146,62 @@ 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_match_limit(self):
res = jsonata.Jsonata("$match('ababab', /ab/)").evaluate(None)
assert res is not None
assert [m["index"] for m in res] == [0, 2, 4]
assert jsonata.Jsonata("$match('ababab', /ab/, 0)").evaluate(None) is None
assert jsonata.Jsonata("$match('ababab', /ab/, 1)").evaluate(None) == {"match": "ab", "index": 0, "groups": []}

res = jsonata.Jsonata("$match('ababab', /ab/, 2)").evaluate(None)
assert res is not None
assert [m["index"] for m in res] == [0, 2]

res = jsonata.Jsonata("$match('ababab', /ab/, 5)").evaluate(None)
assert res is not None
assert [m["index"] for m in res] == [0, 2, 4]

def test_match_fractional_limit(self):
res = jsonata.Jsonata("$match('ababab', /ab/, 4/2)").evaluate(None)
assert res is not None
assert [m["index"] for m in res] == [0, 2]

# Like jsonata-js, matches are returned while the count is less than the limit
res = jsonata.Jsonata("$match('ababab', /ab/, 1.5)").evaluate(None)
assert res is not None
assert [m["index"] for m in res] == [0, 2]

def test_match_limit_stops_searching(self):
searched = []

class CountingPattern:
def __init__(self, pattern):
self._pattern = pattern

def __getattr__(self, name):
return getattr(self._pattern, name)

def finditer(self, string):
for m in self._pattern.finditer(string):
searched.append(m.start())
yield m

def counting_regex_engine(pattern, flags):
return CountingPattern(default_regex_engine(pattern, flags))

# The search stops once the limit is reached
for limit, expected in [(0, []), (1, [0]), (2, [0, 2])]:
searched.clear()
jsonata.Jsonata(f"$match('ababab', /ab/, {limit})", counting_regex_engine).evaluate(None)
assert searched == expected

def test_partial_application_with_map(self):
# Only ? placeholders are unbound, so $map doesn't pass the index as a trailing argument
assert jsonata.Jsonata("$map(['abc', 'abc'], $substring(?, 1))").evaluate(None) == ["bc", "bc"]
assert jsonata.Jsonata("$map(['ab', 'ab'], $match(?, /ab/)).match").evaluate(None) == ["ab", "ab"]

def test_partial_application_optional_args(self):
# Trailing args that weren't supplied aren't passed to the native function
assert jsonata.Jsonata("$sort(?)([3, 1, 2])").evaluate(None) == [1, 2, 3]
assert jsonata.Jsonata("$map([[3, 1, 2], [2, 1]], $sort(?))").evaluate(None) == [[1, 2, 3], [1, 2]]
Loading