diff --git a/frontend/src/lib/csvAnalysis.js b/frontend/src/lib/csvAnalysis.js index d838e52..80c388a 100644 --- a/frontend/src/lib/csvAnalysis.js +++ b/frontend/src/lib/csvAnalysis.js @@ -218,3 +218,15 @@ export function runAnalysis(rows, timestampCol, noteCol, classifications) { consecutiveStats, }; } + +// ── Numeric extraction (for numerical buckets) ──────────────────────────────── + +/** + * Extract the first signed decimal from a string. Returns null if none found. + * Examples: "L25.5" -> 25.5, "R-12" -> -12, "abc" -> null + */ +export function extractNumber(str) { + if (str == null) return null; + const m = String(str).match(/-?\d+(\.\d+)?/); + return m ? parseFloat(m[0]) : null; +} diff --git a/frontend/tests/csvAnalysis.test.js b/frontend/tests/csvAnalysis.test.js index f242b62..14544b9 100644 --- a/frontend/tests/csvAnalysis.test.js +++ b/frontend/tests/csvAnalysis.test.js @@ -3,6 +3,7 @@ import { parseCSV, getUniqueNoteValues, runAnalysis, + extractNumber, } from '../src/lib/csvAnalysis'; // ── Fixtures ─────────────────────────────────────────────────────────────────── @@ -276,3 +277,32 @@ describe('runAnalysis — edge cases', () => { expect(result.consecutiveStats['Failure'].totalRuns).toBe(1); }); }); + +describe('extractNumber', () => { + test('extracts positive decimal from prefixed string', () => { + expect(extractNumber('L25.5')).toBe(25.5); + }); + test('extracts negative integer with letter prefix', () => { + expect(extractNumber('R-12')).toBe(-12); + }); + test('extracts integer from underscore-separated label', () => { + expect(extractNumber('trial_007')).toBe(7); + }); + test('does not parse scientific notation as one number', () => { + // Accepted limitation: regex matches "3.2", strips "e2" + expect(extractNumber('3.2e2')).toBe(3.2); + }); + test('strips trailing units', () => { + expect(extractNumber('42 ms')).toBe(42); + }); + test('returns null for purely alphabetic string', () => { + expect(extractNumber('abc')).toBeNull(); + }); + test('returns null for empty string', () => { + expect(extractNumber('')).toBeNull(); + }); + test('returns null for null/undefined', () => { + expect(extractNumber(null)).toBeNull(); + expect(extractNumber(undefined)).toBeNull(); + }); +});