Full document, spreadsheet, slideshow, and diagram tooling
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request 'fix: FIND/SEARCH tests, Excel-style quote escaping, version pruning tiebreaker' (#249) from fix/batch8-find-tokenizer-pruning into main

scott 5679f887 8cfec9d3

+88 -5
+1 -1
server/index.ts
··· 639 639 db.prepare(` 640 640 DELETE FROM versions WHERE id IN ( 641 641 SELECT id FROM versions WHERE document_id = ? 642 - ORDER BY created_at ASC 642 + ORDER BY created_at ASC, rowid ASC 643 643 LIMIT ? 644 644 ) 645 645 `).run(docId, excess);
+9 -4
src/sheets/formulas.ts
··· 70 70 continue; 71 71 } 72 72 73 - // String literal 73 + // String literal (supports both "" Excel-style and \ backslash escaping) 74 74 if (s[i] === '"') { 75 75 let str = ''; 76 76 i++; // skip opening quote 77 - while (i < s.length && s[i] !== '"') { 78 - if (s[i] === '\\' && i + 1 < s.length) { str += s[++i]; } 79 - else { str += s[i]; } 77 + while (i < s.length) { 78 + if (s[i] === '"') { 79 + // Excel-style escaped quote: "" → " 80 + if (i + 1 < s.length && s[i + 1] === '"') { str += '"'; i += 2; continue; } 81 + break; // closing quote 82 + } 83 + if (s[i] === '\\' && i + 1 < s.length) { str += s[++i]; i++; continue; } 84 + str += s[i]; 80 85 i++; 81 86 } 82 87 i++; // skip closing quote
+78
tests/formulas-edge-cases.test.ts
··· 1416 1416 expect(evalWith('"hello"&" world"')).toBe('hello world'); 1417 1417 }); 1418 1418 }); 1419 + 1420 + // ============================================================ 1421 + // FIND/SEARCH edge cases (#411) 1422 + // ============================================================ 1423 + 1424 + describe('FIND function edge cases', () => { 1425 + it('finds substring at the start', () => { 1426 + expect(evalWith('FIND("He","Hello")')).toBe(1); 1427 + }); 1428 + 1429 + it('finds substring in the middle', () => { 1430 + expect(evalWith('FIND("ll","Hello")')).toBe(3); 1431 + }); 1432 + 1433 + it('returns #VALUE! when not found', () => { 1434 + expect(evalWith('FIND("xyz","Hello")')).toBe('#VALUE!'); 1435 + }); 1436 + 1437 + it('is case-sensitive', () => { 1438 + expect(evalWith('FIND("hello","Hello")')).toBe('#VALUE!'); 1439 + }); 1440 + 1441 + it('finds with start_num parameter', () => { 1442 + expect(evalWith('FIND("l","Hello World",4)')).toBe(4); 1443 + }); 1444 + 1445 + it('finds second occurrence via start_num', () => { 1446 + expect(evalWith('FIND("o","Hello World",5)')).toBe(5); 1447 + expect(evalWith('FIND("o","Hello World",6)')).toBe(8); 1448 + }); 1449 + 1450 + it('finds empty string (returns start position)', () => { 1451 + expect(evalWith('FIND("","Hello")')).toBe(1); 1452 + expect(evalWith('FIND("","Hello",3)')).toBe(3); 1453 + }); 1454 + 1455 + it('finds single character', () => { 1456 + expect(evalWith('FIND("W","Hello World")')).toBe(7); 1457 + }); 1458 + }); 1459 + 1460 + describe('SEARCH function edge cases', () => { 1461 + it('is case-insensitive', () => { 1462 + expect(evalWith('SEARCH("hello","Hello World")')).toBe(1); 1463 + expect(evalWith('SEARCH("WORLD","Hello World")')).toBe(7); 1464 + }); 1465 + 1466 + it('returns #VALUE! when not found', () => { 1467 + expect(evalWith('SEARCH("xyz","Hello")')).toBe('#VALUE!'); 1468 + }); 1469 + 1470 + it('finds with start_num', () => { 1471 + expect(evalWith('SEARCH("o","Hello World",6)')).toBe(8); 1472 + }); 1473 + }); 1474 + 1475 + // ============================================================ 1476 + // String tokenizer — Excel-style double-quote escaping (#416) 1477 + // ============================================================ 1478 + 1479 + describe('String tokenizer — Excel-style double-quote escaping', () => { 1480 + it('double-double-quote produces literal quote', () => { 1481 + // Formula: "He said ""hello""" → He said "hello" 1482 + expect(evalWith('"He said ""hello"""')).toBe('He said "hello"'); 1483 + }); 1484 + 1485 + it('empty string with double-quotes', () => { 1486 + expect(evalWith('""')).toBe(''); 1487 + }); 1488 + 1489 + it('single embedded quote', () => { 1490 + expect(evalWith('"say ""hi"""')).toBe('say "hi"'); 1491 + }); 1492 + 1493 + it('concatenation with embedded quotes', () => { 1494 + expect(evalWith('"A""B"&"C"')).toBe('A"BC'); 1495 + }); 1496 + });