Skip to content

Commit d79dac3

Browse files
Copilotrajbos
andcommitted
Fix CSV parsing issue with trailing whitespace in headers
Co-authored-by: rajbos <6085745+rajbos@users.noreply.github.com>
1 parent b836db6 commit d79dac3

2 files changed

Lines changed: 76 additions & 13 deletions

File tree

src/lib/utils.ts

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -32,16 +32,20 @@ export function parseCSV(csv: string): CopilotUsageData[] {
3232
const expectedHeaders = ['Timestamp', 'User', 'Model', 'Requests Used', 'Exceeds Monthly Quota', 'Total Monthly Quota'];
3333

3434
// Parse header row to check for expected columns
35-
const headerMatches = headerLine.match(/("([^"]*)"|([^,]*))(,|$)/g);
35+
// First, trim any trailing whitespace from the header line
36+
const trimmedHeaderLine = headerLine.trim();
37+
const headerMatches = trimmedHeaderLine.match(/("([^"]*)"|([^,]*))(,|$)/g);
3638
if (!headerMatches || headerMatches.length < 6) {
3739
throw new Error('CSV header must contain at least 6 columns');
3840
}
3941

40-
const headers = headerMatches.map(m =>
41-
m.endsWith(',')
42-
? m.slice(0, -1).replace(/^"(.*)"$/, '$1')
43-
: m.replace(/^"(.*)"$/, '$1')
44-
).filter(h => h.trim() !== '').map(h => h.trim()); // Filter empty strings and trim whitespace
42+
const headers = headerMatches.map(m => {
43+
// Remove trailing comma if present
44+
let processed = m.endsWith(',') ? m.slice(0, -1) : m;
45+
// Remove surrounding quotes if present
46+
processed = processed.replace(/^"(.*)"$/, '$1');
47+
return processed;
48+
}).filter(h => h.trim() !== '').map(h => h.trim()); // Filter empty strings and trim whitespace
4549

4650
// Check if all expected headers are present (case-insensitive exact match)
4751
const missingHeaders = expectedHeaders.filter(expected =>
@@ -71,18 +75,21 @@ export function parseCSV(csv: string): CopilotUsageData[] {
7175

7276
// Skip the header row and process data rows
7377
return lines.slice(1).map((line, index) => {
74-
// Handle quoted CSV properly
75-
const matches = line.match(/("([^"]*)"|([^,]*))(,|$)/g);
78+
// Handle quoted CSV properly - trim any trailing whitespace first
79+
const trimmedLine = line.trim();
80+
const matches = trimmedLine.match(/("([^"]*)"|([^,]*))(,|$)/g);
7681

7782
if (!matches || matches.length < 6) {
7883
throw new Error(`Invalid CSV row format at line ${index + 2}: expected 6 columns, got ${matches ? matches.length : 0}`);
7984
}
8085

81-
const values = matches.map(m =>
82-
m.endsWith(',')
83-
? m.slice(0, -1).replace(/^"(.*)"$/, '$1')
84-
: m.replace(/^"(.*)"$/, '$1')
85-
);
86+
const values = matches.map(m => {
87+
// Remove trailing comma if present
88+
let processed = m.endsWith(',') ? m.slice(0, -1) : m;
89+
// Remove surrounding quotes if present
90+
processed = processed.replace(/^"(.*)"$/, '$1');
91+
return processed;
92+
}).filter(v => v.trim() !== ''); // Filter out empty values
8693

8794
// Validate timestamp
8895
const timestamp = new Date(values[0]);

src/test/csv-user-issue.test.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { parseCSV } from '../lib/utils';
3+
4+
describe('CSV Parsing Issue Reproduction', () => {
5+
it('should handle the exact CSV format provided by user', () => {
6+
const csvContent = `"Timestamp","User","Model","Requests Used","Exceeds Monthly Quota","Total Monthly Quota"
7+
"2025-06-11T05:13:27.8766440Z","xyz","gpt-4.1-2025-04-14","1","False","Unlimited"
8+
"2025-06-11T05:09:40.8432110Z","xyz","gpt-4.1-2025-04-14","1","False","Unlimited"`;
9+
10+
console.log('Testing user CSV content:');
11+
console.log(csvContent);
12+
console.log('');
13+
14+
// This should parse without issues
15+
expect(() => parseCSV(csvContent)).not.toThrow();
16+
17+
const result = parseCSV(csvContent);
18+
expect(result).toHaveLength(2);
19+
expect(result[0].user).toBe('xyz');
20+
expect(result[0].model).toBe('gpt-4.1-2025-04-14');
21+
expect(result[0].requestsUsed).toBe(1);
22+
expect(result[0].exceedsQuota).toBe(false);
23+
expect(result[0].totalMonthlyQuota).toBe('Unlimited');
24+
});
25+
26+
it('should handle header with potential whitespace issues', () => {
27+
// Test with potential whitespace or encoding issues
28+
const csvWithExtraSpacing = `"Timestamp","User","Model","Requests Used","Exceeds Monthly Quota","Total Monthly Quota"
29+
"2025-06-11T05:13:27.8766440Z","xyz","gpt-4.1-2025-04-14","1","False","Unlimited"`;
30+
31+
expect(() => parseCSV(csvWithExtraSpacing)).not.toThrow();
32+
33+
const result = parseCSV(csvWithExtraSpacing);
34+
expect(result).toHaveLength(1);
35+
expect(result[0].totalMonthlyQuota).toBe('Unlimited');
36+
});
37+
38+
it('should handle header without quotes', () => {
39+
const csvWithoutQuotes = `Timestamp,User,Model,Requests Used,Exceeds Monthly Quota,Total Monthly Quota
40+
2025-06-11T05:13:27.8766440Z,xyz,gpt-4.1-2025-04-14,1,False,Unlimited`;
41+
42+
expect(() => parseCSV(csvWithoutQuotes)).not.toThrow();
43+
const result = parseCSV(csvWithoutQuotes);
44+
expect(result).toHaveLength(1);
45+
});
46+
47+
it('should handle data rows with trailing spaces', () => {
48+
const csvWithTrailingSpaceInData = `"Timestamp","User","Model","Requests Used","Exceeds Monthly Quota","Total Monthly Quota"
49+
"2025-06-11T05:13:27.8766440Z","xyz","gpt-4.1-2025-04-14","1","False","Unlimited" `;
50+
51+
expect(() => parseCSV(csvWithTrailingSpaceInData)).not.toThrow();
52+
const result = parseCSV(csvWithTrailingSpaceInData);
53+
expect(result).toHaveLength(1);
54+
expect(result[0].totalMonthlyQuota).toBe('Unlimited');
55+
});
56+
});

0 commit comments

Comments
 (0)