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 case-sensitivity, MOD negative, forms rating + serialization' (#241) from fix/correctness-qa-batch into main

scott 71efc881 713aaa9c

+64 -3
+19 -1
src/forms/main.ts
··· 336 336 questionsEl.appendChild(qEl); 337 337 } 338 338 339 + // Rating star / scale button click handlers 340 + previewPane.querySelectorAll('.form-preview-rating, .form-preview-scale').forEach(container => { 341 + container.addEventListener('click', (e) => { 342 + const btn = (e.target as HTMLElement).closest('[data-value]') as HTMLElement | null; 343 + if (!btn) return; 344 + // Mark selected: remove active from siblings, add to clicked 345 + container.querySelectorAll('[data-value]').forEach(b => b.classList.remove('active')); 346 + btn.classList.add('active'); 347 + (container as HTMLElement).dataset.selectedValue = btn.dataset.value!; 348 + }); 349 + }); 350 + 339 351 // Submit handler 340 352 previewPane.querySelector('#preview-submit')!.addEventListener('click', () => { 341 353 const formAnswers = new Map<string, unknown>(); ··· 355 367 } 356 368 } else if (el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement) { 357 369 formAnswers.set(qid, el.value); 370 + } else if (el instanceof HTMLElement && (el.classList.contains('form-preview-rating') || el.classList.contains('form-preview-scale'))) { 371 + // Rating stars and scale buttons store selection in data-selected-value 372 + const val = el.dataset.selectedValue; 373 + if (val) formAnswers.set(qid, Number(val)); 358 374 } 359 375 }); 360 376 ··· 367 383 368 384 if (errors.size === 0) { 369 385 const response = createResponse(form.id, formAnswers); 370 - yResponses.push([JSON.stringify(response)]); 386 + // Convert Map to plain object for JSON serialization (Map serializes as {}) 387 + const serializable = { ...response, answers: Object.fromEntries(response.answers) }; 388 + yResponses.push([JSON.stringify(serializable)]); 371 389 previewPane.innerHTML = '<div style="text-align:center;padding:3rem"><h2>Response submitted!</h2><p>Your response has been recorded.</p></div>'; 372 390 } 373 391 });
+7 -2
src/sheets/formulas.ts
··· 775 775 case 'ROUNDUP': { const f = Math.pow(10, toNum(args[1] ?? 0)); return Math.ceil(toNum(args[0]) * f) / f; } 776 776 case 'ROUNDDOWN': { const f = Math.pow(10, toNum(args[1] ?? 0)); return Math.floor(toNum(args[0]) * f) / f; } 777 777 case 'INT': return Math.floor(toNum(args[0])); 778 - case 'MOD': { const mDiv = toNum(args[1]); return mDiv === 0 ? '#DIV/0!' : toNum(args[0]) % mDiv; } 778 + case 'MOD': { const mDiv = toNum(args[1]); if (mDiv === 0) return '#DIV/0!'; const mR = toNum(args[0]) % mDiv; return (mR !== 0 && Math.sign(mR) !== Math.sign(mDiv)) ? mR + mDiv : mR; } 779 779 case 'POWER': return Math.pow(toNum(args[0]), toNum(args[1])); 780 780 case 'SQRT': return Math.sqrt(toNum(args[0])); 781 781 case 'LOG': return args.length > 1 ? Math.log(toNum(args[0])) / Math.log(toNum(args[1])) : Math.log10(toNum(args[0])); ··· 821 821 } 822 822 return str.replaceAll(old, rep); 823 823 } 824 - case 'FIND': 824 + case 'FIND': { 825 + // FIND is case-sensitive (per Excel spec) 826 + const idx = String(args[1]).indexOf(String(args[0]), toNum(args[2] ?? 1) - 1); 827 + return idx === -1 ? '#VALUE!' : idx + 1; 828 + } 825 829 case 'SEARCH': { 830 + // SEARCH is case-insensitive 826 831 const idx = String(args[1]).toUpperCase().indexOf(String(args[0]).toUpperCase(), toNum(args[2] ?? 1) - 1); 827 832 return idx === -1 ? '#VALUE!' : idx + 1; 828 833 }
+38
tests/formulas-edge-cases.test.ts
··· 994 994 expect((result as number[])[2]).toBe(3); 995 995 }); 996 996 }); 997 + 998 + // ============================================================ 999 + // QA issue #420 — Formula correctness fixes 1000 + // ============================================================ 1001 + 1002 + describe('FIND vs SEARCH case-sensitivity', () => { 1003 + it('FIND is case-sensitive', () => { 1004 + expect(evalWith('FIND("a","Apple")')).toBe('#VALUE!'); 1005 + expect(evalWith('FIND("A","Apple")')).toBe(1); 1006 + }); 1007 + 1008 + it('SEARCH is case-insensitive', () => { 1009 + expect(evalWith('SEARCH("a","Apple")')).toBe(1); 1010 + expect(evalWith('SEARCH("A","Apple")')).toBe(1); 1011 + }); 1012 + 1013 + it('FIND with start_num works', () => { 1014 + expect(evalWith('FIND("l","Hello",4)')).toBe(4); 1015 + }); 1016 + }); 1017 + 1018 + describe('MOD with negative numbers', () => { 1019 + it('MOD(-7, 3) returns 2 (Excel behavior, not JS -1)', () => { 1020 + expect(evalWith('MOD(-7,3)')).toBe(2); 1021 + }); 1022 + 1023 + it('MOD(7, -3) returns -2 (result sign matches divisor)', () => { 1024 + expect(evalWith('MOD(7,-3)')).toBe(-2); 1025 + }); 1026 + 1027 + it('MOD(-7, -3) returns -1', () => { 1028 + expect(evalWith('MOD(-7,-3)')).toBe(-1); 1029 + }); 1030 + 1031 + it('MOD(10, 3) returns 1 (positive case unchanged)', () => { 1032 + expect(evalWith('MOD(10,3)')).toBe(1); 1033 + }); 1034 + });