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
19 changes: 6 additions & 13 deletions src/Lua/Standard/StringLibrary.cs
Original file line number Diff line number Diff line change
Expand Up @@ -513,12 +513,8 @@ CancellationToken cancellationToken
var anchor = pattern.Length > 0 && pattern[0] == '^';
var pIdx = anchor ? 1 : 0;

// For empty patterns, we need to match at every position including after the last character
var sEndIdx =
s.Length
+ (pattern.Length == 0 || (anchor && pattern.Length == 1) ? 1 : 0);

for (var sIdx = start; sIdx < sEndIdx; sIdx++)
// Include the end position so patterns can match an empty string there.
for (var sIdx = start; sIdx <= s.Length; sIdx++)
{
// Reset match state for each attempt
matchState.Level = 0;
Expand Down Expand Up @@ -604,9 +600,8 @@ CancellationToken cancellationToken
var anchor = pattern.Length > 0 && pattern[0] == '^';
var sIdx = 0;

// For empty patterns, we need to match at every position including after the last character
var sEndIdx = s.Length + (pattern.Length == 0 || (anchor && pattern.Length == 1) ? 1 : 0);
while (sIdx < sEndIdx && replaceCount < n)
// Include the end position so patterns can match an empty string there.
while (sIdx <= s.Length && replaceCount < n)
{
// Reset match state for each attempt
matchState.Level = 0;
Expand Down Expand Up @@ -898,10 +893,8 @@ bool find
var anchor = pattern.Length > 0 && pattern[0] == '^';
var pIdx = anchor ? 1 : 0;

// For empty patterns, we need to match at every position including after the last character
var sEndIdx = s.Length + (pattern.Length == 0 ? 1 : 0);

for (var sIdx = init; sIdx < sEndIdx; sIdx++)
// Include the end position so patterns can match an empty string there.
for (var sIdx = init; sIdx <= s.Length; sIdx++)
{
// Reset match state for each attempt
matchState.Level = 0;
Expand Down
142 changes: 142 additions & 0 deletions tests/Lua.Tests/PatternEndPositionTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
using Lua.Standard;

namespace Lua.Tests;

public class PatternEndPositionTests
{
[TestCase("abc", "$", 1, 4, 3)]
[TestCase("abc", "$", -1, 4, 3)]
[TestCase("abc", "a*", 4, 4, 3)]
[TestCase("abc", ".*", 4, 4, 3)]
[TestCase("", "$", 1, 1, 0)]
[TestCase("", "^$", 1, 1, 0)]
[TestCase("", "^", 1, 1, 0)]
[TestCase("", "a*", 1, 1, 0)]
[TestCase("", "a?", 1, 1, 0)]
[TestCase("", "a-", 1, 1, 0)]
public async Task FindEmptyMatch(string source, string pattern, int init, int start, int end)
{
using var state = LuaState.Create();
state.OpenStringLibrary();

var result = await state.DoStringAsync(
$"return string.find('{source}', '{pattern}', {init})"
);

Assert.That(result, Is.EqualTo(new LuaValue[] { start, end }));
}

[TestCase("abc", "$", 1)]
[TestCase("abc", "a*", 4)]
[TestCase("abc", ".*", 4)]
[TestCase("", "$", 1)]
[TestCase("", "^$", 1)]
[TestCase("", "^", 1)]
[TestCase("", "a*", 1)]
[TestCase("", "a?", 1)]
[TestCase("", "a-", 1)]
public async Task MatchEmptyString(string source, string pattern, int init)
{
using var state = LuaState.Create();
state.OpenStringLibrary();

var result = await state.DoStringAsync(
$"return string.match('{source}', '{pattern}', {init})"
);

Assert.That(result, Is.EqualTo(new LuaValue[] { "" }));
}

[TestCase("abc", 4)]
[TestCase("", 1)]
public async Task PositionCaptureAtEnd(string source, int init)
{
using var state = LuaState.Create();
state.OpenStringLibrary();

var result = await state.DoStringAsync(
$"""
local start, finish, position = string.find('{source}', '()', {init})
return start, finish, position, string.match('{source}', '()', {init})
"""
);

Assert.That(result, Is.EqualTo(new LuaValue[] { init, init - 1, init, init }));
}

[TestCase("abc", "a+", 4)]
[TestCase("abc", "a*", 5)]
[TestCase("", "a+", 1)]
[TestCase("", "^a", 1)]
[TestCase("", "a*", 2)]
public async Task RejectNonemptyMatchOrStartBeyondEnd(string source, string pattern, int init)
{
using var state = LuaState.Create();
state.OpenStringLibrary();

var result = await state.DoStringAsync(
$"""
return string.find('{source}', '{pattern}', {init}),
string.match('{source}', '{pattern}', {init})
"""
);

Assert.That(result, Is.EqualTo(new LuaValue[] { LuaValue.Nil, LuaValue.Nil }));
}

[TestCase("", "$", "", 1)]
[TestCase("a", "$", "", 1)]
[TestCase("", "a*", "", 1)]
[TestCase("a", "a*", "a,", 2)]
[TestCase("abc", "a*", "a,,,", 4)]
[TestCase("", "()", "1", 1)]
[TestCase("abc", "()", "1,2,3,4", 4)]
[TestCase("abc", "", ",,,", 4)]
public async Task GMatchIncludesEnd(string source, string pattern, string expected, int count)
{
using var state = LuaState.Create();
state.OpenStandardLibraries();

var result = await state.DoStringAsync(
$$"""
local source = '{{source}}'
local matches = {}
for value in string.gmatch(source, '{{pattern}}') do
matches[#matches + 1] = value
if #matches > #source + 1 then error('iterator did not terminate') end
end
return table.concat(matches, ','), #matches
"""
);

Assert.That(result, Is.EqualTo(new LuaValue[] { expected, count }));
}

[TestCase("abc", "$", 4, "abcX", 1)]
[TestCase("", "$", 1, "X", 1)]
[TestCase("", "^$", 1, "X", 1)]
[TestCase("", "a*", 1, "X", 1)]
[TestCase("abc", "()", 4, "XaXbXcX", 4)]
[TestCase("a", "a*", 2, "XX", 2)]
[TestCase("a", "a*", 1, "X", 1)]
[TestCase("abc", "$", 0, "abc", 0)]
[TestCase("", "a+", 1, "", 0)]
[TestCase("abc", "", 4, "XaXbXcX", 4)]
public async Task GSubIncludesEnd(
string source,
string pattern,
int limit,
string expected,
int count
)
{
using var state = LuaState.Create();
state.OpenStringLibrary();

var result = await state.DoStringAsync(
$"return string.gsub('{source}', '{pattern}', 'X', {limit})"
);

Assert.That(result, Is.EqualTo(new LuaValue[] { expected, count }));
}
}
2 changes: 1 addition & 1 deletion tests/Lua.Tests/PatternMatchingTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -374,7 +374,7 @@ public async Task Test_StringGMatch_EmptyMatches()
return count
"
);
Assert.That(result[0].Read<double>(), Is.EqualTo(3));
Assert.That(result[0].Read<double>(), Is.EqualTo(4)); // Includes the empty match after 'c'.
}

[Test]
Expand Down
Loading