PatternMatchingTests.cs 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801
  1. using Lua.Standard;
  2. namespace Lua.Tests;
  3. public class PatternMatchingTests
  4. {
  5. [Test]
  6. public async Task Test_StringMatch_BasicPatterns()
  7. {
  8. var state = LuaState.Create();
  9. state.OpenStringLibrary();
  10. // Literal match
  11. var result = await state.DoStringAsync("return string.match('hello world', 'hello')");
  12. Assert.That(result[0].Read<string>(), Is.EqualTo("hello"));
  13. result = await state.DoStringAsync("return string.match('hello world', 'world')");
  14. Assert.That(result[0].Read<string>(), Is.EqualTo("world"));
  15. // No match
  16. result = await state.DoStringAsync("return string.match('hello world', 'xyz')");
  17. Assert.That(result[0].Type, Is.EqualTo(LuaValueType.Nil));
  18. }
  19. [Test]
  20. public async Task Test_StringMatch_CharacterClasses()
  21. {
  22. var state = LuaState.Create();
  23. state.OpenStringLibrary();
  24. // %d - digits
  25. var result = await state.DoStringAsync("return string.match('hello123', '%d')");
  26. Assert.That(result[0].Read<string>(), Is.EqualTo("1"));
  27. result = await state.DoStringAsync("return string.match('hello123', '%d+')");
  28. Assert.That(result[0].Read<string>(), Is.EqualTo("123"));
  29. // %a - letters
  30. result = await state.DoStringAsync("return string.match('123hello', '%a+')");
  31. Assert.That(result[0].Read<string>(), Is.EqualTo("hello"));
  32. // %w - alphanumeric
  33. result = await state.DoStringAsync("return string.match('test_123', '%w+')");
  34. Assert.That(result[0].Read<string>(), Is.EqualTo("test"));
  35. // %s - whitespace
  36. result = await state.DoStringAsync("return string.match('hello world', '%s')");
  37. Assert.That(result[0].Read<string>(), Is.EqualTo(" "));
  38. }
  39. [Test]
  40. public async Task Test_StringMatch_Quantifiers()
  41. {
  42. var state = LuaState.Create();
  43. state.OpenStringLibrary();
  44. // + (one or more)
  45. var result = await state.DoStringAsync("return string.match('aaa', 'a+')");
  46. Assert.That(result[0].Read<string>(), Is.EqualTo("aaa"));
  47. // * (zero or more)
  48. result = await state.DoStringAsync("return string.match('bbb', 'a*b')");
  49. Assert.That(result[0].Read<string>(), Is.EqualTo("b"));
  50. result = await state.DoStringAsync("return string.match('aaab', 'a*b')");
  51. Assert.That(result[0].Read<string>(), Is.EqualTo("aaab"));
  52. // ? (optional)
  53. result = await state.DoStringAsync("return string.match('color', 'colou?r')");
  54. Assert.That(result[0].Read<string>(), Is.EqualTo("color"));
  55. result = await state.DoStringAsync("return string.match('colour', 'colou?r')");
  56. Assert.That(result[0].Read<string>(), Is.EqualTo("colour"));
  57. // - (minimal repetition)
  58. result = await state.DoStringAsync("return string.match('aaab', 'a-b')");
  59. Assert.That(result[0].Read<string>(), Is.EqualTo("aaab"));
  60. }
  61. [Test]
  62. public async Task Test_StringMatch_Captures()
  63. {
  64. var state = LuaState.Create();
  65. state.OpenStringLibrary();
  66. // Single capture
  67. var result = await state.DoStringAsync("return string.match('hello world', '(%a+)')");
  68. Assert.That(result[0].Read<string>(), Is.EqualTo("hello"));
  69. // Multiple captures
  70. result = await state.DoStringAsync("return string.match('hello world', '(%a+) (%a+)')");
  71. Assert.That(result, Has.Length.EqualTo(2));
  72. Assert.That(result[0].Read<string>(), Is.EqualTo("hello"));
  73. Assert.That(result[1].Read<string>(), Is.EqualTo("world"));
  74. // Position capture
  75. result = await state.DoStringAsync("return string.match('hello', '()llo')");
  76. Assert.That(result[0].Read<double>(), Is.EqualTo(3));
  77. // Email pattern
  78. result = await state.DoStringAsync("return string.match('[email protected]', '(%w+)@(%w+)%.(%w+)')");
  79. Assert.That(result, Has.Length.EqualTo(3));
  80. Assert.That(result[0].Read<string>(), Is.EqualTo("test"));
  81. Assert.That(result[1].Read<string>(), Is.EqualTo("example"));
  82. Assert.That(result[2].Read<string>(), Is.EqualTo("com"));
  83. }
  84. [Test]
  85. public async Task Test_StringMatch_Anchors()
  86. {
  87. var state = LuaState.Create();
  88. state.OpenStringLibrary();
  89. // ^ (start anchor)
  90. var result = await state.DoStringAsync("return string.match('hello world', '^hello')");
  91. Assert.That(result[0].Read<string>(), Is.EqualTo("hello"));
  92. result = await state.DoStringAsync("return string.match('hello world', '^world')");
  93. Assert.That(result[0].Type, Is.EqualTo(LuaValueType.Nil));
  94. // $ (end anchor)
  95. result = await state.DoStringAsync("return string.match('hello world', 'world$')");
  96. Assert.That(result[0].Read<string>(), Is.EqualTo("world"));
  97. result = await state.DoStringAsync("return string.match('hello world', 'hello$')");
  98. Assert.That(result[0].Type, Is.EqualTo(LuaValueType.Nil));
  99. }
  100. [Test]
  101. public async Task Test_StringMatch_WithInitPosition()
  102. {
  103. var state = LuaState.Create();
  104. state.OpenStringLibrary();
  105. // Start from specific position
  106. var result = await state.DoStringAsync("return string.match('hello world', 'o', 5)");
  107. Assert.That(result[0].Read<string>(), Is.EqualTo("o"));
  108. result = await state.DoStringAsync("return string.match('hello world', 'o', 8)");
  109. Assert.That(result[0].Read<string>(), Is.EqualTo("o"));
  110. // Negative init (from end)
  111. result = await state.DoStringAsync("return string.match('hello', 'l', -2)");
  112. Assert.That(result[0].Read<string>(), Is.EqualTo("l"));
  113. }
  114. [Test]
  115. public async Task Test_StringMatch_SpecialPatterns()
  116. {
  117. var state = LuaState.Create();
  118. state.OpenStringLibrary();
  119. // Dot (any character)
  120. var result = await state.DoStringAsync("return string.match('hello', 'h.llo')");
  121. Assert.That(result[0].Read<string>(), Is.EqualTo("hello"));
  122. // Character sets
  123. result = await state.DoStringAsync("return string.match('hello123', '[0-9]+')");
  124. Assert.That(result[0].Read<string>(), Is.EqualTo("123"));
  125. result = await state.DoStringAsync("return string.match('Hello', '[Hh]ello')");
  126. Assert.That(result[0].Read<string>(), Is.EqualTo("Hello"));
  127. // Negated character sets
  128. result = await state.DoStringAsync("return string.match('hello123', '[^a-z]+')");
  129. Assert.That(result[0].Read<string>(), Is.EqualTo("123"));
  130. }
  131. [Test]
  132. public async Task Test_StringFind_BasicUsage()
  133. {
  134. var state = LuaState.Create();
  135. state.OpenStringLibrary();
  136. // Basic literal search
  137. var result = await state.DoStringAsync("return string.find('hello world', 'world')");
  138. Assert.That(result.Length, Is.EqualTo(2));
  139. Assert.That(result[0].Read<double>(), Is.EqualTo(7)); // Start position (1-based)
  140. Assert.That(result[1].Read<double>(), Is.EqualTo(11)); // End position (1-based)
  141. // Search with start position
  142. result = await state.DoStringAsync("return string.find('hello hello', 'hello', 3)");
  143. Assert.That(result.Length, Is.EqualTo(2));
  144. Assert.That(result[0].Read<double>(), Is.EqualTo(7)); // Second occurrence
  145. Assert.That(result[1].Read<double>(), Is.EqualTo(11));
  146. // No match
  147. result = await state.DoStringAsync("return string.find('hello world', 'xyz')");
  148. Assert.That(result.Length, Is.EqualTo(1));
  149. Assert.That(result[0].Type, Is.EqualTo(LuaValueType.Nil));
  150. }
  151. [Test]
  152. public async Task Test_StringFind_WithPatterns()
  153. {
  154. var state = LuaState.Create();
  155. state.OpenStringLibrary();
  156. // Pattern with captures
  157. var result = await state.DoStringAsync("return string.find('hello 123', '(%a+) (%d+)')");
  158. Assert.That(result.Length, Is.EqualTo(4)); // start, end, capture1, capture2
  159. Assert.That(result[0].Read<double>(), Is.EqualTo(1)); // Start position
  160. Assert.That(result[1].Read<double>(), Is.EqualTo(9)); // End position
  161. Assert.That(result[2].Read<string>(), Is.EqualTo("hello")); // First capture
  162. Assert.That(result[3].Read<string>(), Is.EqualTo("123")); // Second capture
  163. // Character class patterns
  164. result = await state.DoStringAsync("return string.find('abc123def', '%d+')");
  165. Assert.That(result.Length, Is.EqualTo(2));
  166. Assert.That(result[0].Read<double>(), Is.EqualTo(4)); // Position of '123'
  167. Assert.That(result[1].Read<double>(), Is.EqualTo(6));
  168. // Anchored patterns
  169. result = await state.DoStringAsync("return string.find('hello world', '^hello')");
  170. Assert.That(result.Length, Is.EqualTo(2));
  171. Assert.That(result[0].Read<double>(), Is.EqualTo(1));
  172. Assert.That(result[1].Read<double>(), Is.EqualTo(5));
  173. result = await state.DoStringAsync("return string.find('hello world', '^world')");
  174. Assert.That(result.Length, Is.EqualTo(1));
  175. Assert.That(result[0].Type, Is.EqualTo(LuaValueType.Nil));
  176. }
  177. [Test]
  178. public async Task Test_StringFind_PlainSearch()
  179. {
  180. var state = LuaState.Create();
  181. state.OpenStringLibrary();
  182. // Plain search (4th parameter = true)
  183. var result = await state.DoStringAsync("return string.find('hello (world)', '(world)', 1, true)");
  184. Assert.That(result.Length, Is.EqualTo(2));
  185. Assert.That(result[0].Read<double>(), Is.EqualTo(7)); // Start of '(world)'
  186. Assert.That(result[1].Read<double>(), Is.EqualTo(13)); // End of '(world)'
  187. // Pattern search would fail but plain search succeeds
  188. result = await state.DoStringAsync("return string.find('test%d+test', '%d+', 1, true)");
  189. Assert.That(result.Length, Is.EqualTo(2));
  190. Assert.That(result[0].Read<double>(), Is.EqualTo(5)); // Literal '%d+'
  191. Assert.That(result[1].Read<double>(), Is.EqualTo(7));
  192. }
  193. [Test]
  194. public async Task Test_StringFind_EdgeCases()
  195. {
  196. var state = LuaState.Create();
  197. state.OpenStringLibrary();
  198. // Empty pattern
  199. var result = await state.DoStringAsync("return string.find('hello', '')");
  200. Assert.That(result.Length, Is.EqualTo(2));
  201. Assert.That(result[0].Read<double>(), Is.EqualTo(1));
  202. Assert.That(result[1].Read<double>(), Is.EqualTo(0));
  203. // Negative start position
  204. result = await state.DoStringAsync("return string.find('hello', 'l', -2)");
  205. Assert.That(result.Length, Is.EqualTo(2));
  206. Assert.That(result[0].Read<double>(), Is.EqualTo(4)); // Last 'l'
  207. Assert.That(result[1].Read<double>(), Is.EqualTo(4));
  208. // Start position beyond string length
  209. result = await state.DoStringAsync("return string.find('hello', 'l', 10)");
  210. Assert.That(result.Length, Is.EqualTo(1));
  211. Assert.That(result[0].Type, Is.EqualTo(LuaValueType.Nil));
  212. // Position captures
  213. result = await state.DoStringAsync("return string.find('hello', '()l()l()')");
  214. Assert.That(result.Length, Is.EqualTo(5)); // start, end, pos1, pos2, pos3
  215. Assert.That(result[0].Read<double>(), Is.EqualTo(3)); // Start of match
  216. Assert.That(result[1].Read<double>(), Is.EqualTo(4)); // End of match
  217. Assert.That(result[2].Read<double>(), Is.EqualTo(3)); // Position before first 'l'
  218. Assert.That(result[3].Read<double>(), Is.EqualTo(4)); // Position before second 'l'
  219. Assert.That(result[4].Read<double>(), Is.EqualTo(5)); // Position after second 'l'
  220. }
  221. [Test]
  222. public async Task Test_StringGMatch_BasicUsage()
  223. {
  224. var state = LuaState.Create();
  225. state.OpenStringLibrary();
  226. state.OpenTableLibrary();
  227. // Test basic gmatch iteration
  228. var result = await state.DoStringAsync(@"
  229. local words = {}
  230. for word in string.gmatch('hello world lua', '%a+') do
  231. table.insert(words, word)
  232. end
  233. return table.unpack(words)
  234. ");
  235. Assert.That(result.Length, Is.EqualTo(3));
  236. Assert.That(result[0].Read<string>(), Is.EqualTo("hello"));
  237. Assert.That(result[1].Read<string>(), Is.EqualTo("world"));
  238. Assert.That(result[2].Read<string>(), Is.EqualTo("lua"));
  239. }
  240. [Test]
  241. public async Task Test_StringGMatch_WithCaptures()
  242. {
  243. var state = LuaState.Create();
  244. state.OpenStringLibrary();
  245. state.OpenTableLibrary();
  246. // Test gmatch with captures
  247. var result = await state.DoStringAsync(@"
  248. local pairs = {}
  249. for key, value in string.gmatch('a=1 b=2 c=3', '(%a)=(%d)') do
  250. table.insert(pairs, key .. ':' .. value)
  251. end
  252. return table.unpack(pairs)
  253. ");
  254. Assert.That(result.Length, Is.EqualTo(3));
  255. Assert.That(result[0].Read<string>(), Is.EqualTo("a:1"));
  256. Assert.That(result[1].Read<string>(), Is.EqualTo("b:2"));
  257. Assert.That(result[2].Read<string>(), Is.EqualTo("c:3"));
  258. }
  259. [Test]
  260. public async Task Test_StringGMatch_Numbers()
  261. {
  262. var state = LuaState.Create();
  263. state.OpenStringLibrary();
  264. state.OpenTableLibrary();
  265. // Extract all numbers from a string
  266. var result = await state.DoStringAsync(@"
  267. local numbers = {}
  268. for num in string.gmatch('price: $12.50, tax: $2.75, total: $15.25', '%d+%.%d+') do
  269. table.insert(numbers, num)
  270. end
  271. return table.unpack(numbers)
  272. ");
  273. Assert.That(result.Length, Is.EqualTo(3));
  274. Assert.That(result[0].Read<string>(), Is.EqualTo("12.50"));
  275. Assert.That(result[1].Read<string>(), Is.EqualTo("2.75"));
  276. Assert.That(result[2].Read<string>(), Is.EqualTo("15.25"));
  277. }
  278. [Test]
  279. public async Task Test_StringGMatch_EmptyMatches()
  280. {
  281. var state = LuaState.Create();
  282. state.OpenStringLibrary();
  283. state.OpenTableLibrary();
  284. // Test with pattern that can match empty strings
  285. var result = await state.DoStringAsync(@"
  286. local count = 0
  287. for match in string.gmatch('abc', 'a*') do
  288. count = count + 1
  289. if count > 10 then break end -- Prevent infinite loop
  290. end
  291. return count
  292. ");
  293. Assert.That(result[0].Read<double>(), Is.EqualTo(3));
  294. }
  295. [Test]
  296. public async Task Test_StringGMatch_ComplexPatterns()
  297. {
  298. var state = LuaState.Create();
  299. state.OpenStringLibrary();
  300. state.OpenTableLibrary();
  301. // Extract email-like patterns
  302. var result = await state.DoStringAsync(@"
  303. local emails = {}
  304. local text = 'Contact us at [email protected] or [email protected] for help'
  305. for email in string.gmatch(text, '%w+@%w+%.%w+') do
  306. table.insert(emails, email)
  307. end
  308. return table.unpack(emails)
  309. ");
  310. Assert.That(result.Length, Is.EqualTo(2));
  311. Assert.That(result[0].Read<string>(), Is.EqualTo("[email protected]"));
  312. Assert.That(result[1].Read<string>(), Is.EqualTo("[email protected]"));
  313. }
  314. [Test]
  315. public async Task Test_StringGMatch_PositionCaptures()
  316. {
  317. var state = LuaState.Create();
  318. state.OpenStringLibrary();
  319. state.OpenTableLibrary();
  320. // Test position captures with gmatch
  321. var result = await state.DoStringAsync(@"
  322. local positions = {}
  323. for pos, char in string.gmatch('hello', '()(%a)') do
  324. table.insert(positions, pos .. ':' .. char)
  325. end
  326. return table.unpack(positions)
  327. ");
  328. Assert.That(result.Length, Is.EqualTo(5));
  329. Assert.That(result[0].Read<string>(), Is.EqualTo("1:h"));
  330. Assert.That(result[1].Read<string>(), Is.EqualTo("2:e"));
  331. Assert.That(result[2].Read<string>(), Is.EqualTo("3:l"));
  332. Assert.That(result[3].Read<string>(), Is.EqualTo("4:l"));
  333. Assert.That(result[4].Read<string>(), Is.EqualTo("5:o"));
  334. }
  335. [Test]
  336. public async Task Test_StringGMatch_NoMatches()
  337. {
  338. var state = LuaState.Create();
  339. state.OpenStringLibrary();
  340. state.OpenTableLibrary();
  341. // Test when no matches are found
  342. var result = await state.DoStringAsync(@"
  343. local count = 0
  344. for match in string.gmatch('hello world', '%d+') do
  345. count = count + 1
  346. end
  347. return count
  348. ");
  349. Assert.That(result[0].Read<double>(), Is.EqualTo(0));
  350. }
  351. [Test]
  352. public async Task Test_StringGMatch_SingleCharacter()
  353. {
  354. var state = LuaState.Create();
  355. state.OpenStringLibrary();
  356. state.OpenTableLibrary();
  357. // Test matching single characters
  358. var result = await state.DoStringAsync(@"
  359. local chars = {}
  360. for char in string.gmatch('a1b2c3', '%a') do
  361. table.insert(chars, char)
  362. end
  363. return table.unpack(chars)
  364. ");
  365. Assert.That(result.Length, Is.EqualTo(3));
  366. Assert.That(result[0].Read<string>(), Is.EqualTo("a"));
  367. Assert.That(result[1].Read<string>(), Is.EqualTo("b"));
  368. Assert.That(result[2].Read<string>(), Is.EqualTo("c"));
  369. }
  370. [Test]
  371. public async Task Test_StringFind_And_GMatch_Consistency()
  372. {
  373. var state = LuaState.Create();
  374. state.OpenStringLibrary();
  375. // Test that find and gmatch work consistently with the same pattern
  376. var result = await state.DoStringAsync(@"
  377. local text = 'The quick brown fox jumps over the lazy dog'
  378. -- Find first word
  379. local start, end_pos, word1 = string.find(text, '(%a+)')
  380. -- Get first word from gmatch
  381. local word2 = string.gmatch(text, '%a+')()
  382. return word1, word2, start, end_pos
  383. ");
  384. Assert.That(result.Length, Is.EqualTo(4));
  385. Assert.That(result[0].Read<string>(), Is.EqualTo("The")); // From find
  386. Assert.That(result[1].Read<string>(), Is.EqualTo("The")); // From gmatch
  387. Assert.That(result[2].Read<double>(), Is.EqualTo(1)); // Start position
  388. Assert.That(result[3].Read<double>(), Is.EqualTo(3)); // End position
  389. }
  390. [Test]
  391. public async Task Test_Pattern_NegatedCharacterClassWithCapture()
  392. {
  393. var state = LuaState.Create();
  394. state.OpenStringLibrary();
  395. // Test the problematic pattern ^([^:]*):
  396. var result = await state.DoStringAsync(@"
  397. local text = 'key:value'
  398. local match = string.match(text, '^([^:]*):')
  399. return match
  400. ");
  401. Assert.That(result.Length, Is.EqualTo(1));
  402. Assert.That(result[0].Read<string>(), Is.EqualTo("key"));
  403. // Test with empty match
  404. result = await state.DoStringAsync(@"
  405. local text = ':value'
  406. local match = string.match(text, '^([^:]*):')
  407. return match
  408. ");
  409. Assert.That(result.Length, Is.EqualTo(1));
  410. Assert.That(result[0].Read<string>(), Is.EqualTo("")); // Empty string
  411. // Test with multiple captures
  412. result = await state.DoStringAsync(@"
  413. local text = '[key]:[value]:extra'
  414. local a, b = string.match(text, '^([^:]*):([^:]*)')
  415. return a, b
  416. ");
  417. Assert.That(result.Length, Is.EqualTo(2));
  418. Assert.That(result[0].Read<string>(), Is.EqualTo("[key]"));
  419. Assert.That(result[1].Read<string>(), Is.EqualTo("[value]"));
  420. }
  421. [Test]
  422. public async Task Test_StringGSub_BasicReplacements()
  423. {
  424. var state = LuaState.Create();
  425. state.OpenStringLibrary();
  426. // Simple string replacement
  427. var result = await state.DoStringAsync("return string.gsub('hello world', 'world', 'lua')");
  428. Assert.That(result.Length, Is.EqualTo(2));
  429. Assert.That(result[0].Read<string>(), Is.EqualTo("hello lua"));
  430. Assert.That(result[1].Read<double>(), Is.EqualTo(1)); // Replacement count
  431. // Multiple replacements
  432. result = await state.DoStringAsync("return string.gsub('hello hello hello', 'hello', 'hi')");
  433. Assert.That(result.Length, Is.EqualTo(2));
  434. Assert.That(result[0].Read<string>(), Is.EqualTo("hi hi hi"));
  435. Assert.That(result[1].Read<double>(), Is.EqualTo(3));
  436. // Limited replacements
  437. result = await state.DoStringAsync("return string.gsub('hello hello hello', 'hello', 'hi', 2)");
  438. Assert.That(result.Length, Is.EqualTo(2));
  439. Assert.That(result[0].Read<string>(), Is.EqualTo("hi hi hello"));
  440. Assert.That(result[1].Read<double>(), Is.EqualTo(2));
  441. }
  442. [Test]
  443. public async Task Test_StringGSub_PatternReplacements()
  444. {
  445. var state = LuaState.Create();
  446. state.OpenStringLibrary();
  447. // Character class patterns
  448. var result = await state.DoStringAsync("return string.gsub('hello123world456', '%d+', 'X')");
  449. Assert.That(result.Length, Is.EqualTo(2));
  450. Assert.That(result[0].Read<string>(), Is.EqualTo("helloXworldX"));
  451. Assert.That(result[1].Read<double>(), Is.EqualTo(2));
  452. // Capture replacements
  453. result = await state.DoStringAsync("return string.gsub('John Doe', '(%a+) (%a+)', '%2, %1')");
  454. Assert.That(result.Length, Is.EqualTo(2));
  455. Assert.That(result[0].Read<string>(), Is.EqualTo("Doe, John"));
  456. Assert.That(result[1].Read<double>(), Is.EqualTo(1));
  457. // Whole match replacement (%0)
  458. result = await state.DoStringAsync("return string.gsub('test123', '%d+', '[%0]')");
  459. Assert.That(result.Length, Is.EqualTo(2));
  460. Assert.That(result[0].Read<string>(), Is.EqualTo("test[123]"));
  461. Assert.That(result[1].Read<double>(), Is.EqualTo(1));
  462. }
  463. [Test]
  464. public async Task Test_StringGSub_FunctionReplacements()
  465. {
  466. var state = LuaState.Create();
  467. state.OpenStringLibrary();
  468. // Function replacement
  469. var result = await state.DoStringAsync(@"
  470. return string.gsub('hello world', '%a+', function(s)
  471. return s:upper()
  472. end)
  473. ");
  474. Assert.That(result.Length, Is.EqualTo(2));
  475. Assert.That(result[0].Read<string>(), Is.EqualTo("HELLO WORLD"));
  476. Assert.That(result[1].Read<double>(), Is.EqualTo(2));
  477. // Function with position captures
  478. result = await state.DoStringAsync(@"
  479. return string.gsub('hello', '()l', function(pos)
  480. return '[' .. pos .. ']'
  481. end)
  482. ");
  483. Assert.That(result.Length, Is.EqualTo(2));
  484. Assert.That(result[0].Read<string>(), Is.EqualTo("he[3][4]o"));
  485. Assert.That(result[1].Read<double>(), Is.EqualTo(2));
  486. // Function returning nil (no replacement)
  487. result = await state.DoStringAsync(@"
  488. return string.gsub('a1b2c3', '%d', function(s)
  489. if s == '2' then return nil end
  490. return 'X'
  491. end)
  492. ");
  493. Assert.That(result.Length, Is.EqualTo(2));
  494. Assert.That(result[0].Read<string>(), Is.EqualTo("aXb2cX"));
  495. Assert.That(result[1].Read<double>(), Is.EqualTo(3)); // Only 2 replacements made
  496. }
  497. [Test]
  498. public async Task Test_StringGSub_TableReplacements()
  499. {
  500. var state = LuaState.Create();
  501. state.OpenStringLibrary();
  502. // Table replacement
  503. var result = await state.DoStringAsync(@"
  504. local map = {hello = 'hi', world = 'lua'}
  505. return string.gsub('hello world', '%a+', map)
  506. ");
  507. Assert.That(result.Length, Is.EqualTo(2));
  508. Assert.That(result[0].Read<string>(), Is.EqualTo("hi lua"));
  509. Assert.That(result[1].Read<double>(), Is.EqualTo(2));
  510. // Table with missing keys (no replacement)
  511. result = await state.DoStringAsync(@"
  512. local map = {hello = 'hi'}
  513. return string.gsub('hello world', '%a+', map)
  514. ");
  515. Assert.That(result.Length, Is.EqualTo(2));
  516. Assert.That(result[0].Read<string>(), Is.EqualTo("hi world"));
  517. Assert.That(result[1].Read<double>(), Is.EqualTo(2)); // Only 'hello' was replaced
  518. }
  519. [Test]
  520. public async Task Test_StringGSub_EmptyPattern()
  521. {
  522. var state = LuaState.Create();
  523. state.OpenStringLibrary();
  524. // Empty pattern should match at every position
  525. var result = await state.DoStringAsync("return string.gsub('abc', '', '.')");
  526. Assert.That(result.Length, Is.EqualTo(2));
  527. Assert.That(result[0].Read<string>(), Is.EqualTo(".a.b.c."));
  528. Assert.That(result[1].Read<double>(), Is.EqualTo(4)); // 4 positions: before a, before b, before c, after c
  529. }
  530. [Test]
  531. public async Task Test_StringGSub_BalancedPatterns()
  532. {
  533. var state = LuaState.Create();
  534. state.OpenStringLibrary();
  535. // Balanced parentheses pattern
  536. var result = await state.DoStringAsync(@"
  537. return string.gsub('(hello) and (world)', '%b()', function(s)
  538. return s:upper()
  539. end)
  540. ");
  541. Assert.That(result.Length, Is.EqualTo(2));
  542. Assert.That(result[0].Read<string>(), Is.EqualTo("(HELLO) and (WORLD)"));
  543. Assert.That(result[1].Read<double>(), Is.EqualTo(2));
  544. // Balanced brackets
  545. result = await state.DoStringAsync("return string.gsub('[a][b][c]', '%b[]', 'X')");
  546. Assert.That(result.Length, Is.EqualTo(2));
  547. Assert.That(result[0].Read<string>(), Is.EqualTo("XXX"));
  548. Assert.That(result[1].Read<double>(), Is.EqualTo(3));
  549. }
  550. [Test]
  551. public async Task Test_StringGSub_EscapeSequences()
  552. {
  553. var state = LuaState.Create();
  554. state.OpenStringLibrary();
  555. // Test %% escape (literal %)
  556. var result = await state.DoStringAsync("return string.gsub('test', 'test', '100%%')");
  557. Assert.That(result.Length, Is.EqualTo(2));
  558. Assert.That(result[0].Read<string>(), Is.EqualTo("100%"));
  559. Assert.That(result[1].Read<double>(), Is.EqualTo(1));
  560. }
  561. [Test]
  562. public async Task Test_StringGSub_EdgeCases()
  563. {
  564. var state = LuaState.Create();
  565. state.OpenStringLibrary();
  566. // Empty string
  567. var result = await state.DoStringAsync("return string.gsub('', 'a', 'b')");
  568. Assert.That(result.Length, Is.EqualTo(2));
  569. Assert.That(result[0].Read<string>(), Is.EqualTo(""));
  570. Assert.That(result[1].Read<double>(), Is.EqualTo(0));
  571. // No matches
  572. result = await state.DoStringAsync("return string.gsub('hello', 'xyz', 'abc')");
  573. Assert.That(result.Length, Is.EqualTo(2));
  574. Assert.That(result[0].Read<string>(), Is.EqualTo("hello"));
  575. Assert.That(result[1].Read<double>(), Is.EqualTo(0));
  576. // Zero replacement limit
  577. result = await state.DoStringAsync("return string.gsub('hello hello', 'hello', 'hi', 0)");
  578. Assert.That(result.Length, Is.EqualTo(2));
  579. Assert.That(result[0].Read<string>(), Is.EqualTo("hello hello"));
  580. Assert.That(result[1].Read<double>(), Is.EqualTo(0));
  581. }
  582. [Test]
  583. public async Task Test_StringGSub_ComplexPatterns()
  584. {
  585. var state = LuaState.Create();
  586. state.OpenStringLibrary();
  587. // Email replacement
  588. var result = await state.DoStringAsync(@"
  589. local text = 'Contact [email protected] or [email protected]'
  590. return string.gsub(text, '(%w+)@(%w+)%.(%w+)', function(user, domain, tld)
  591. return user:upper() .. '@' .. domain:upper() .. '.' .. tld:upper()
  592. end)
  593. ");
  594. Assert.That(result.Length, Is.EqualTo(2));
  595. Assert.That(result[0].Read<string>(), Is.EqualTo("Contact [email protected] or [email protected]"));
  596. Assert.That(result[1].Read<double>(), Is.EqualTo(2));
  597. // URL path extraction
  598. result = await state.DoStringAsync(@"
  599. return string.gsub('http://example.com/path/to/file.html',
  600. '^https?://[^/]+(/.*)', '%1')
  601. ");
  602. Assert.That(result.Length, Is.EqualTo(2));
  603. Assert.That(result[0].Read<string>(), Is.EqualTo("/path/to/file.html"));
  604. Assert.That(result[1].Read<double>(), Is.EqualTo(1));
  605. }
  606. [Test]
  607. public async Task Test_PatternMatching_Consistency()
  608. {
  609. var state = LuaState.Create();
  610. state.OpenStringLibrary();
  611. // Test that all string functions work consistently with same patterns
  612. var result = await state.DoStringAsync(@"
  613. local text = 'The quick brown fox jumps over the lazy dog'
  614. local pattern = '%a+'
  615. -- Test find
  616. local start, end_pos, word = string.find(text, '(' .. pattern .. ')')
  617. -- Test match
  618. local match = string.match(text, pattern)
  619. -- Test gsub count
  620. local _, count = string.gsub(text, pattern, function(s) return s end)
  621. -- Test gmatch count
  622. local gmatch_count = 0
  623. for word in string.gmatch(text, pattern) do
  624. gmatch_count = gmatch_count + 1
  625. end
  626. return word, match, count, gmatch_count, start, end_pos
  627. ");
  628. Assert.That(result.Length, Is.EqualTo(6));
  629. Assert.That(result[0].Read<string>(), Is.EqualTo("The")); // find capture
  630. Assert.That(result[1].Read<string>(), Is.EqualTo("The")); // match result
  631. Assert.That(result[2].Read<double>(), Is.EqualTo(9)); // gsub count (9 words)
  632. Assert.That(result[3].Read<double>(), Is.EqualTo(9)); // gmatch count
  633. Assert.That(result[4].Read<double>(), Is.EqualTo(1)); // find start
  634. Assert.That(result[5].Read<double>(), Is.EqualTo(3)); // find end
  635. }
  636. [Test]
  637. public async Task Test_PatternMatching_SpecialPatterns()
  638. {
  639. var state = LuaState.Create();
  640. state.OpenStringLibrary();
  641. // Frontier pattern %f
  642. var result = await state.DoStringAsync(@"
  643. return string.gsub('hello world', '%f[%a]', '[')
  644. ");
  645. Assert.That(result.Length, Is.EqualTo(2));
  646. Assert.That(result[0].Read<string>(), Is.EqualTo("[hello [world"));
  647. Assert.That(result[1].Read<double>(), Is.EqualTo(2));
  648. // Minimal repetition with -
  649. result = await state.DoStringAsync("return string.match('aaab', 'a-b')");
  650. Assert.That(result[0].Read<string>(), Is.EqualTo("aaab"));
  651. // Optional quantifier ?
  652. result = await state.DoStringAsync("return string.gsub('color colour', 'colou?r', 'COLOR')");
  653. Assert.That(result.Length, Is.EqualTo(2));
  654. Assert.That(result[0].Read<string>(), Is.EqualTo("COLOR COLOR"));
  655. Assert.That(result[1].Read<double>(), Is.EqualTo(2));
  656. }
  657. [Test]
  658. public async Task Test_PatternMatching_ErrorCases()
  659. {
  660. var state = LuaState.Create();
  661. state.OpenStringLibrary();
  662. // Invalid pattern - missing closing bracket
  663. var exception = Assert.ThrowsAsync<LuaRuntimeException>(async () =>
  664. await state.DoStringAsync("return string.match('test', '[abc')"));
  665. Assert.That(exception.Message, Does.Contain("missing ']'"));
  666. // Invalid pattern - missing %b arguments
  667. exception = Assert.ThrowsAsync<LuaRuntimeException>(async () =>
  668. await state.DoStringAsync("return string.match('test', '%b')"));
  669. Assert.That(exception.Message, Does.Contain("missing arguments to '%b'"));
  670. // Pattern too complex (exceeds recursion limit)
  671. exception = Assert.ThrowsAsync<LuaRuntimeException>(async () =>
  672. await state.DoStringAsync("return string.match(string.rep('a', 1000), string.rep('a?', 1000) .. string.rep('a', 1000))"));
  673. Assert.That(exception.Message, Does.Contain("pattern too complex"));
  674. }
  675. }