PatternMatchingTests.cs 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935
  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 = LuaGlobalState.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 = LuaGlobalState.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 = LuaGlobalState.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 = LuaGlobalState.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 = LuaGlobalState.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 = LuaGlobalState.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 = LuaGlobalState.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 = LuaGlobalState.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 = LuaGlobalState.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 = LuaGlobalState.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 = LuaGlobalState.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. // Empty pattern with empty string (should match at position 1)
  204. result = await state.DoStringAsync("return string.find('', '')");
  205. Assert.That(result.Length, Is.EqualTo(2));
  206. Assert.That(result[0].Read<double>(), Is.EqualTo(1));
  207. Assert.That(result[1].Read<double>(), Is.EqualTo(0));
  208. // Negative start position
  209. result = await state.DoStringAsync("return string.find('hello', 'l', -2)");
  210. Assert.That(result.Length, Is.EqualTo(2));
  211. Assert.That(result[0].Read<double>(), Is.EqualTo(4)); // Last 'l'
  212. Assert.That(result[1].Read<double>(), Is.EqualTo(4));
  213. // Start position beyond string length
  214. result = await state.DoStringAsync("return string.find('hello', 'l', 10)");
  215. Assert.That(result.Length, Is.EqualTo(1));
  216. Assert.That(result[0].Type, Is.EqualTo(LuaValueType.Nil));
  217. // Empty string with init beyond length
  218. result = await state.DoStringAsync("return string.find('', '', 2)");
  219. Assert.That(result.Length, Is.EqualTo(1));
  220. Assert.That(result[0].Type, Is.EqualTo(LuaValueType.Nil));
  221. // Position captures
  222. result = await state.DoStringAsync("return string.find('hello', '()l()l()')");
  223. Assert.That(result.Length, Is.EqualTo(5)); // start, end, pos1, pos2, pos3
  224. Assert.That(result[0].Read<double>(), Is.EqualTo(3)); // Start of match
  225. Assert.That(result[1].Read<double>(), Is.EqualTo(4)); // End of match
  226. Assert.That(result[2].Read<double>(), Is.EqualTo(3)); // Position before first 'l'
  227. Assert.That(result[3].Read<double>(), Is.EqualTo(4)); // Position before second 'l'
  228. Assert.That(result[4].Read<double>(), Is.EqualTo(5)); // Position after second 'l'
  229. }
  230. [Test]
  231. public async Task Test_StringGMatch_BasicUsage()
  232. {
  233. var state = LuaGlobalState.Create();
  234. state.OpenStringLibrary();
  235. state.OpenTableLibrary();
  236. // Test basic gmatch iteration
  237. var result = await state.DoStringAsync(@"
  238. local words = {}
  239. for word in string.gmatch('hello world lua', '%a+') do
  240. table.insert(words, word)
  241. end
  242. return table.unpack(words)
  243. ");
  244. Assert.That(result.Length, Is.EqualTo(3));
  245. Assert.That(result[0].Read<string>(), Is.EqualTo("hello"));
  246. Assert.That(result[1].Read<string>(), Is.EqualTo("world"));
  247. Assert.That(result[2].Read<string>(), Is.EqualTo("lua"));
  248. }
  249. [Test]
  250. public async Task Test_StringGMatch_WithCaptures()
  251. {
  252. var state = LuaGlobalState.Create();
  253. state.OpenStringLibrary();
  254. state.OpenTableLibrary();
  255. // Test gmatch with captures
  256. var result = await state.DoStringAsync(@"
  257. local pairs = {}
  258. for key, value in string.gmatch('a=1 b=2 c=3', '(%a)=(%d)') do
  259. table.insert(pairs, key .. ':' .. value)
  260. end
  261. return table.unpack(pairs)
  262. ");
  263. Assert.That(result.Length, Is.EqualTo(3));
  264. Assert.That(result[0].Read<string>(), Is.EqualTo("a:1"));
  265. Assert.That(result[1].Read<string>(), Is.EqualTo("b:2"));
  266. Assert.That(result[2].Read<string>(), Is.EqualTo("c:3"));
  267. }
  268. [Test]
  269. public async Task Test_StringGMatch_Numbers()
  270. {
  271. var state = LuaGlobalState.Create();
  272. state.OpenStringLibrary();
  273. state.OpenTableLibrary();
  274. // Extract all numbers from a string
  275. var result = await state.DoStringAsync(@"
  276. local numbers = {}
  277. for num in string.gmatch('price: $12.50, tax: $2.75, total: $15.25', '%d+%.%d+') do
  278. table.insert(numbers, num)
  279. end
  280. return table.unpack(numbers)
  281. ");
  282. Assert.That(result.Length, Is.EqualTo(3));
  283. Assert.That(result[0].Read<string>(), Is.EqualTo("12.50"));
  284. Assert.That(result[1].Read<string>(), Is.EqualTo("2.75"));
  285. Assert.That(result[2].Read<string>(), Is.EqualTo("15.25"));
  286. }
  287. [Test]
  288. public async Task Test_StringGMatch_EmptyMatches()
  289. {
  290. var state = LuaGlobalState.Create();
  291. state.OpenStringLibrary();
  292. state.OpenTableLibrary();
  293. // Test with pattern that can match empty strings
  294. var result = await state.DoStringAsync(@"
  295. local count = 0
  296. for match in string.gmatch('abc', 'a*') do
  297. count = count + 1
  298. if count > 10 then break end -- Prevent infinite loop
  299. end
  300. return count
  301. ");
  302. Assert.That(result[0].Read<double>(), Is.EqualTo(3));
  303. }
  304. [Test]
  305. public async Task Test_StringGMatch_ComplexPatterns()
  306. {
  307. var state = LuaGlobalState.Create();
  308. state.OpenStringLibrary();
  309. state.OpenTableLibrary();
  310. // Extract email-like patterns
  311. var result = await state.DoStringAsync(@"
  312. local emails = {}
  313. local text = 'Contact us at [email protected] or [email protected] for help'
  314. for email in string.gmatch(text, '%w+@%w+%.%w+') do
  315. table.insert(emails, email)
  316. end
  317. return table.unpack(emails)
  318. ");
  319. Assert.That(result.Length, Is.EqualTo(2));
  320. Assert.That(result[0].Read<string>(), Is.EqualTo("[email protected]"));
  321. Assert.That(result[1].Read<string>(), Is.EqualTo("[email protected]"));
  322. }
  323. [Test]
  324. public async Task Test_StringGMatch_PositionCaptures()
  325. {
  326. var state = LuaGlobalState.Create();
  327. state.OpenStringLibrary();
  328. state.OpenTableLibrary();
  329. // Test position captures with gmatch
  330. var result = await state.DoStringAsync(@"
  331. local positions = {}
  332. for pos, char in string.gmatch('hello', '()(%a)') do
  333. table.insert(positions, pos .. ':' .. char)
  334. end
  335. return table.unpack(positions)
  336. ");
  337. Assert.That(result.Length, Is.EqualTo(5));
  338. Assert.That(result[0].Read<string>(), Is.EqualTo("1:h"));
  339. Assert.That(result[1].Read<string>(), Is.EqualTo("2:e"));
  340. Assert.That(result[2].Read<string>(), Is.EqualTo("3:l"));
  341. Assert.That(result[3].Read<string>(), Is.EqualTo("4:l"));
  342. Assert.That(result[4].Read<string>(), Is.EqualTo("5:o"));
  343. }
  344. [Test]
  345. public async Task Test_StringGMatch_NoMatches()
  346. {
  347. var state = LuaGlobalState.Create();
  348. state.OpenStringLibrary();
  349. state.OpenTableLibrary();
  350. // Test when no matches are found
  351. var result = await state.DoStringAsync(@"
  352. local count = 0
  353. for match in string.gmatch('hello world', '%d+') do
  354. count = count + 1
  355. end
  356. return count
  357. ");
  358. Assert.That(result[0].Read<double>(), Is.EqualTo(0));
  359. }
  360. [Test]
  361. public async Task Test_StringGMatch_SingleCharacter()
  362. {
  363. var state = LuaGlobalState.Create();
  364. state.OpenStringLibrary();
  365. state.OpenTableLibrary();
  366. // Test matching single characters
  367. var result = await state.DoStringAsync(@"
  368. local chars = {}
  369. for char in string.gmatch('a1b2c3', '%a') do
  370. table.insert(chars, char)
  371. end
  372. return table.unpack(chars)
  373. ");
  374. Assert.That(result.Length, Is.EqualTo(3));
  375. Assert.That(result[0].Read<string>(), Is.EqualTo("a"));
  376. Assert.That(result[1].Read<string>(), Is.EqualTo("b"));
  377. Assert.That(result[2].Read<string>(), Is.EqualTo("c"));
  378. }
  379. [Test]
  380. public async Task Test_StringFind_And_GMatch_Consistency()
  381. {
  382. var state = LuaGlobalState.Create();
  383. state.OpenStringLibrary();
  384. // Test that find and gmatch work consistently with the same pattern
  385. var result = await state.DoStringAsync(@"
  386. local text = 'The quick brown fox jumps over the lazy dog'
  387. -- Find first word
  388. local start, end_pos, word1 = string.find(text, '(%a+)')
  389. -- Get first word from gmatch
  390. local word2 = string.gmatch(text, '%a+')()
  391. return word1, word2, start, end_pos
  392. ");
  393. Assert.That(result.Length, Is.EqualTo(4));
  394. Assert.That(result[0].Read<string>(), Is.EqualTo("The")); // From find
  395. Assert.That(result[1].Read<string>(), Is.EqualTo("The")); // From gmatch
  396. Assert.That(result[2].Read<double>(), Is.EqualTo(1)); // Start position
  397. Assert.That(result[3].Read<double>(), Is.EqualTo(3)); // End position
  398. }
  399. [Test]
  400. public async Task Test_Pattern_NegatedCharacterClassWithCapture()
  401. {
  402. var state = LuaGlobalState.Create();
  403. state.OpenStringLibrary();
  404. // Test the problematic pattern ^([^:]*):
  405. var result = await state.DoStringAsync(@"
  406. local text = 'key:value'
  407. local match = string.match(text, '^([^:]*):')
  408. return match
  409. ");
  410. Assert.That(result.Length, Is.EqualTo(1));
  411. Assert.That(result[0].Read<string>(), Is.EqualTo("key"));
  412. // Test with empty match
  413. result = await state.DoStringAsync(@"
  414. local text = ':value'
  415. local match = string.match(text, '^([^:]*):')
  416. return match
  417. ");
  418. Assert.That(result.Length, Is.EqualTo(1));
  419. Assert.That(result[0].Read<string>(), Is.EqualTo("")); // Empty string
  420. // Test with multiple captures
  421. result = await state.DoStringAsync(@"
  422. local text = '[key]:[value]:extra'
  423. local a, b = string.match(text, '^([^:]*):([^:]*)')
  424. return a, b
  425. ");
  426. Assert.That(result.Length, Is.EqualTo(2));
  427. Assert.That(result[0].Read<string>(), Is.EqualTo("[key]"));
  428. Assert.That(result[1].Read<string>(), Is.EqualTo("[value]"));
  429. }
  430. [Test]
  431. public async Task Test_StringGSub_BasicReplacements()
  432. {
  433. var state = LuaGlobalState.Create();
  434. state.OpenStringLibrary();
  435. // Simple string replacement
  436. var result = await state.DoStringAsync("return string.gsub('hello world', 'world', 'lua')");
  437. Assert.That(result.Length, Is.EqualTo(2));
  438. Assert.That(result[0].Read<string>(), Is.EqualTo("hello lua"));
  439. Assert.That(result[1].Read<double>(), Is.EqualTo(1)); // Replacement count
  440. // Multiple replacements
  441. result = await state.DoStringAsync("return string.gsub('hello hello hello', 'hello', 'hi')");
  442. Assert.That(result.Length, Is.EqualTo(2));
  443. Assert.That(result[0].Read<string>(), Is.EqualTo("hi hi hi"));
  444. Assert.That(result[1].Read<double>(), Is.EqualTo(3));
  445. // Limited replacements
  446. result = await state.DoStringAsync("return string.gsub('hello hello hello', 'hello', 'hi', 2)");
  447. Assert.That(result.Length, Is.EqualTo(2));
  448. Assert.That(result[0].Read<string>(), Is.EqualTo("hi hi hello"));
  449. Assert.That(result[1].Read<double>(), Is.EqualTo(2));
  450. }
  451. [Test]
  452. public async Task Test_StringGSub_PatternReplacements()
  453. {
  454. var state = LuaGlobalState.Create();
  455. state.OpenStringLibrary();
  456. // Character class patterns
  457. var result = await state.DoStringAsync("return string.gsub('hello123world456', '%d+', 'X')");
  458. Assert.That(result.Length, Is.EqualTo(2));
  459. Assert.That(result[0].Read<string>(), Is.EqualTo("helloXworldX"));
  460. Assert.That(result[1].Read<double>(), Is.EqualTo(2));
  461. // Capture replacements
  462. result = await state.DoStringAsync("return string.gsub('John Doe', '(%a+) (%a+)', '%2, %1')");
  463. Assert.That(result.Length, Is.EqualTo(2));
  464. Assert.That(result[0].Read<string>(), Is.EqualTo("Doe, John"));
  465. Assert.That(result[1].Read<double>(), Is.EqualTo(1));
  466. // Whole match replacement (%0)
  467. result = await state.DoStringAsync("return string.gsub('test123', '%d+', '[%0]')");
  468. Assert.That(result.Length, Is.EqualTo(2));
  469. Assert.That(result[0].Read<string>(), Is.EqualTo("test[123]"));
  470. Assert.That(result[1].Read<double>(), Is.EqualTo(1));
  471. }
  472. [Test]
  473. public async Task Test_StringGSub_FunctionReplacements()
  474. {
  475. var state = LuaGlobalState.Create();
  476. state.OpenStringLibrary();
  477. // Function replacement
  478. var result = await state.DoStringAsync(@"
  479. return string.gsub('hello world', '%a+', function(s)
  480. return s:upper()
  481. end)
  482. ");
  483. Assert.That(result.Length, Is.EqualTo(2));
  484. Assert.That(result[0].Read<string>(), Is.EqualTo("HELLO WORLD"));
  485. Assert.That(result[1].Read<double>(), Is.EqualTo(2));
  486. // Function with position captures
  487. result = await state.DoStringAsync(@"
  488. return string.gsub('hello', '()l', function(pos)
  489. return '[' .. pos .. ']'
  490. end)
  491. ");
  492. Assert.That(result.Length, Is.EqualTo(2));
  493. Assert.That(result[0].Read<string>(), Is.EqualTo("he[3][4]o"));
  494. Assert.That(result[1].Read<double>(), Is.EqualTo(2));
  495. // Function returning nil (no replacement)
  496. result = await state.DoStringAsync(@"
  497. return string.gsub('a1b2c3', '%d', function(s)
  498. if s == '2' then return nil end
  499. return 'X'
  500. end)
  501. ");
  502. Assert.That(result.Length, Is.EqualTo(2));
  503. Assert.That(result[0].Read<string>(), Is.EqualTo("aXb2cX"));
  504. Assert.That(result[1].Read<double>(), Is.EqualTo(3)); // Only 2 replacements made
  505. }
  506. [Test]
  507. public async Task Test_StringGSub_TableReplacements()
  508. {
  509. var state = LuaGlobalState.Create();
  510. state.OpenStringLibrary();
  511. // Table replacement
  512. var result = await state.DoStringAsync(@"
  513. local map = {hello = 'hi', world = 'lua'}
  514. return string.gsub('hello world', '%a+', map)
  515. ");
  516. Assert.That(result.Length, Is.EqualTo(2));
  517. Assert.That(result[0].Read<string>(), Is.EqualTo("hi lua"));
  518. Assert.That(result[1].Read<double>(), Is.EqualTo(2));
  519. // Table with missing keys (no replacement)
  520. result = await state.DoStringAsync(@"
  521. local map = {hello = 'hi'}
  522. return string.gsub('hello world', '%a+', map)
  523. ");
  524. Assert.That(result.Length, Is.EqualTo(2));
  525. Assert.That(result[0].Read<string>(), Is.EqualTo("hi world"));
  526. Assert.That(result[1].Read<double>(), Is.EqualTo(2)); // Only 'hello' was replaced
  527. }
  528. [Test]
  529. public async Task Test_StringGSub_EmptyPattern()
  530. {
  531. var state = LuaGlobalState.Create();
  532. state.OpenStringLibrary();
  533. // Empty pattern should match at every position
  534. var result = await state.DoStringAsync("return string.gsub('abc', '', '.')");
  535. Assert.That(result.Length, Is.EqualTo(2));
  536. Assert.That(result[0].Read<string>(), Is.EqualTo(".a.b.c."));
  537. Assert.That(result[1].Read<double>(), Is.EqualTo(4)); // 4 positions: before a, before b, before c, after c
  538. }
  539. [Test]
  540. public async Task Test_StringGSub_BalancedPatterns()
  541. {
  542. var state = LuaGlobalState.Create();
  543. state.OpenStringLibrary();
  544. // Balanced parentheses pattern
  545. var result = await state.DoStringAsync(@"
  546. return string.gsub('(hello) and (world)', '%b()', function(s)
  547. return s:upper()
  548. end)
  549. ");
  550. Assert.That(result.Length, Is.EqualTo(2));
  551. Assert.That(result[0].Read<string>(), Is.EqualTo("(HELLO) and (WORLD)"));
  552. Assert.That(result[1].Read<double>(), Is.EqualTo(2));
  553. // Balanced brackets
  554. result = await state.DoStringAsync("return string.gsub('[a][b][c]', '%b[]', 'X')");
  555. Assert.That(result.Length, Is.EqualTo(2));
  556. Assert.That(result[0].Read<string>(), Is.EqualTo("XXX"));
  557. Assert.That(result[1].Read<double>(), Is.EqualTo(3));
  558. }
  559. [Test]
  560. public async Task Test_StringGSub_EscapeSequences()
  561. {
  562. var state = LuaGlobalState.Create();
  563. state.OpenStringLibrary();
  564. // Test %% escape (literal %)
  565. var result = await state.DoStringAsync("return string.gsub('test', 'test', '100%%')");
  566. Assert.That(result.Length, Is.EqualTo(2));
  567. Assert.That(result[0].Read<string>(), Is.EqualTo("100%"));
  568. Assert.That(result[1].Read<double>(), Is.EqualTo(1));
  569. }
  570. [Test]
  571. public async Task Test_StringGSub_EdgeCases()
  572. {
  573. var state = LuaGlobalState.Create();
  574. state.OpenStringLibrary();
  575. // Empty string
  576. var result = await state.DoStringAsync("return string.gsub('', 'a', 'b')");
  577. Assert.That(result.Length, Is.EqualTo(2));
  578. Assert.That(result[0].Read<string>(), Is.EqualTo(""));
  579. Assert.That(result[1].Read<double>(), Is.EqualTo(0));
  580. // No matches
  581. result = await state.DoStringAsync("return string.gsub('hello', 'xyz', 'abc')");
  582. Assert.That(result.Length, Is.EqualTo(2));
  583. Assert.That(result[0].Read<string>(), Is.EqualTo("hello"));
  584. Assert.That(result[1].Read<double>(), Is.EqualTo(0));
  585. // Zero replacement limit
  586. result = await state.DoStringAsync("return string.gsub('hello hello', 'hello', 'hi', 0)");
  587. Assert.That(result.Length, Is.EqualTo(2));
  588. Assert.That(result[0].Read<string>(), Is.EqualTo("hello hello"));
  589. Assert.That(result[1].Read<double>(), Is.EqualTo(0));
  590. }
  591. [Test]
  592. public async Task Test_StringGSub_ComplexPatterns()
  593. {
  594. var state = LuaGlobalState.Create();
  595. state.OpenStringLibrary();
  596. // Email replacement
  597. var result = await state.DoStringAsync(@"
  598. local text = 'Contact [email protected] or [email protected]'
  599. return string.gsub(text, '(%w+)@(%w+)%.(%w+)', function(user, domain, tld)
  600. return user:upper() .. '@' .. domain:upper() .. '.' .. tld:upper()
  601. end)
  602. ");
  603. Assert.That(result.Length, Is.EqualTo(2));
  604. Assert.That(result[0].Read<string>(), Is.EqualTo("Contact [email protected] or [email protected]"));
  605. Assert.That(result[1].Read<double>(), Is.EqualTo(2));
  606. // URL path extraction
  607. result = await state.DoStringAsync(@"
  608. return string.gsub('http://example.com/path/to/file.html',
  609. '^https?://[^/]+(/.*)', '%1')
  610. ");
  611. Assert.That(result.Length, Is.EqualTo(2));
  612. Assert.That(result[0].Read<string>(), Is.EqualTo("/path/to/file.html"));
  613. Assert.That(result[1].Read<double>(), Is.EqualTo(1));
  614. }
  615. [Test]
  616. public async Task Test_PatternMatching_Consistency()
  617. {
  618. var state = LuaGlobalState.Create();
  619. state.OpenStringLibrary();
  620. // Test that all string functions work consistently with same patterns
  621. var result = await state.DoStringAsync(@"
  622. local text = 'The quick brown fox jumps over the lazy dog'
  623. local pattern = '%a+'
  624. -- Test find
  625. local start, end_pos, word = string.find(text, '(' .. pattern .. ')')
  626. -- Test match
  627. local match = string.match(text, pattern)
  628. -- Test gsub count
  629. local _, count = string.gsub(text, pattern, function(s) return s end)
  630. -- Test gmatch count
  631. local gmatch_count = 0
  632. for word in string.gmatch(text, pattern) do
  633. gmatch_count = gmatch_count + 1
  634. end
  635. return word, match, count, gmatch_count, start, end_pos
  636. ");
  637. Assert.That(result.Length, Is.EqualTo(6));
  638. Assert.That(result[0].Read<string>(), Is.EqualTo("The")); // find capture
  639. Assert.That(result[1].Read<string>(), Is.EqualTo("The")); // match result
  640. Assert.That(result[2].Read<double>(), Is.EqualTo(9)); // gsub count (9 words)
  641. Assert.That(result[3].Read<double>(), Is.EqualTo(9)); // gmatch count
  642. Assert.That(result[4].Read<double>(), Is.EqualTo(1)); // find start
  643. Assert.That(result[5].Read<double>(), Is.EqualTo(3)); // find end
  644. }
  645. [Test]
  646. public async Task Test_PatternMatching_SpecialPatterns()
  647. {
  648. var state = LuaGlobalState.Create();
  649. state.OpenStringLibrary();
  650. // Frontier pattern %f
  651. var result = await state.DoStringAsync(@"
  652. return string.gsub('hello world', '%f[%a]', '[')
  653. ");
  654. Assert.That(result.Length, Is.EqualTo(2));
  655. Assert.That(result[0].Read<string>(), Is.EqualTo("[hello [world"));
  656. Assert.That(result[1].Read<double>(), Is.EqualTo(2));
  657. // Minimal repetition with -
  658. result = await state.DoStringAsync("return string.match('aaab', 'a-b')");
  659. Assert.That(result[0].Read<string>(), Is.EqualTo("aaab"));
  660. // Optional quantifier ?
  661. result = await state.DoStringAsync("return string.gsub('color colour', 'colou?r', 'COLOR')");
  662. Assert.That(result.Length, Is.EqualTo(2));
  663. Assert.That(result[0].Read<string>(), Is.EqualTo("COLOR COLOR"));
  664. Assert.That(result[1].Read<double>(), Is.EqualTo(2));
  665. }
  666. [Test]
  667. public void Test_PatternMatching_ErrorCases()
  668. {
  669. var state = LuaGlobalState.Create();
  670. state.OpenStringLibrary();
  671. // Invalid pattern - missing closing bracket
  672. var exception = Assert.ThrowsAsync<LuaRuntimeException>(async () =>
  673. await state.DoStringAsync("return string.match('test', '[abc')"));
  674. Assert.That(exception.Message, Does.Contain("missing ']'"));
  675. // Invalid pattern - missing %b arguments
  676. exception = Assert.ThrowsAsync<LuaRuntimeException>(async () =>
  677. await state.DoStringAsync("return string.match('test', '%b')"));
  678. Assert.That(exception.Message, Does.Contain("missing arguments to '%b'"));
  679. // Pattern too complex (exceeds recursion limit)
  680. exception = Assert.ThrowsAsync<LuaRuntimeException>(async () =>
  681. await state.DoStringAsync("return string.match(string.rep('a', 1000), string.rep('a?', 1000) .. string.rep('a', 1000))"));
  682. Assert.That(exception.Message, Does.Contain("pattern too complex"));
  683. }
  684. [Test]
  685. public async Task Test_DollarSignPattern_EscapingIssue()
  686. {
  687. var state = LuaGlobalState.Create();
  688. state.OpenStringLibrary();
  689. state.OpenTableLibrary();
  690. // Test the problematic pattern from the user's code
  691. // The pattern "$([^$]+)" won't work because $ needs to be escaped as %$
  692. var result = await state.DoStringAsync(@"
  693. local prog = 'Hello $world$ and $123$ test'
  694. local matches = {}
  695. -- Wrong pattern (will not match correctly)
  696. for s in string.gmatch(prog, '$([^$]+)') do
  697. table.insert(matches, s)
  698. end
  699. return #matches
  700. ");
  701. Assert.That(result[0].Read<double>(), Is.EqualTo(4));
  702. // Test the correct pattern with escaped dollar signs
  703. result = await state.DoStringAsync(@"
  704. local prog = 'Hello $world$ and $123$ test'
  705. local matches = {}
  706. -- Correct pattern (with escaped dollar signs)
  707. for s in string.gmatch(prog, '%$([^%$]+)') do
  708. table.insert(matches, s)
  709. end
  710. return table.unpack(matches)
  711. ");
  712. Assert.That(result.Length, Is.EqualTo(4));
  713. Assert.That(result[0].Read<string>(), Is.EqualTo("world"));
  714. Assert.That(result[1].Read<string>(), Is.EqualTo(" and "));
  715. Assert.That(result[2].Read<string>(), Is.EqualTo("123"));
  716. Assert.That(result[3].Read<string>(), Is.EqualTo(" test"));
  717. }
  718. [Test]
  719. public async Task Test_DollarSignPattern_CompleteExample()
  720. {
  721. var state = LuaGlobalState.Create();
  722. state.OpenStringLibrary();
  723. state.OpenTableLibrary();
  724. state.OpenBasicLibrary();
  725. // Simulate the user's use case with corrected pattern
  726. var result = await state.DoStringAsync(@"
  727. local prog = 'Start $1$ middle $hello$ end $2$'
  728. local F = {
  729. [1] = function() return 'FIRST' end,
  730. [2] = function() return 'SECOND' end
  731. }
  732. local output = {}
  733. -- Process the string with correct pattern
  734. local lastPos = 1
  735. for match, content in string.gmatch(prog, '()%$([^%$]+)%$()') do
  736. -- Add text before the match
  737. if match > lastPos then
  738. table.insert(output, prog:sub(lastPos, match - 1))
  739. end
  740. -- Process the content
  741. local n = tonumber(content)
  742. if n and F[n] then
  743. table.insert(output, F[n]())
  744. else
  745. table.insert(output, content)
  746. end
  747. lastPos = match + #content + 2 -- +2 for the two $ signs
  748. end
  749. -- Add remaining text
  750. if lastPos <= #prog then
  751. table.insert(output, prog:sub(lastPos))
  752. end
  753. return table.concat(output)
  754. ");
  755. Assert.That(result[0].Read<string>(), Is.EqualTo("Start FIRST middle hello end SECOND"));
  756. }
  757. [Test]
  758. public async Task Test_DollarSignPattern_EdgeCases()
  759. {
  760. var state = LuaGlobalState.Create();
  761. state.OpenStringLibrary();
  762. state.OpenTableLibrary();
  763. // Test empty content between dollar signs
  764. var result = await state.DoStringAsync(@"
  765. local matches = {}
  766. for s in string.gmatch('$$ and $empty$', '%$([^%$]*)') do
  767. table.insert(matches, s)
  768. end
  769. return table.unpack(matches)
  770. ");
  771. Assert.That(result.Length, Is.EqualTo(4));
  772. Assert.That(result[0].Read<string>(), Is.EqualTo("")); // Empty match
  773. Assert.That(result[1].Read<string>(), Is.EqualTo(" and ")); // Match with spaces
  774. Assert.That(result[2].Read<string>(), Is.EqualTo("empty"));
  775. Assert.That(result[3].Read<string>(), Is.EqualTo("")); // Trailing empty match
  776. // Test nested or adjacent dollar signs
  777. result = await state.DoStringAsync(@"
  778. local matches = {}
  779. for s in string.gmatch('$a$$b$', '%$([^%$]+)') do
  780. table.insert(matches, s)
  781. end
  782. return table.unpack(matches)
  783. ");
  784. Assert.That(result.Length, Is.EqualTo(2));
  785. Assert.That(result[0].Read<string>(), Is.EqualTo("a"));
  786. Assert.That(result[1].Read<string>(), Is.EqualTo("b"));
  787. }
  788. }