From e141504bd900a2d5ffb95d10eb3f2b0d38069cb2 Mon Sep 17 00:00:00 2001 From: bytebrujo Date: Sat, 7 Feb 2026 13:18:27 +0800 Subject: [PATCH] feat: add getMeta() for parse metadata output Add CSVMeta interface and getMeta() method exposing PapaParse-compatible parse metadata: delimiter, linebreak, aborted, truncated, fields array, and elapsedMs. Available before and after iteration. Closes #14 Co-Authored-By: Claude Opus 4.6 --- src/ts/index.ts | 2 +- src/ts/parser.ts | 34 +++++++++++ test/unit/meta.test.ts | 130 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 165 insertions(+), 1 deletion(-) create mode 100644 test/unit/meta.test.ts diff --git a/src/ts/index.ts b/src/ts/index.ts index 86ae0a9..93b8e8e 100644 --- a/src/ts/index.ts +++ b/src/ts/index.ts @@ -4,7 +4,7 @@ * @module turbocsv */ -export { CSVParser, type CSVParserOptions, type StepResult, type ChunkResult, type ParseMeta, type ParserHandle } from "./parser"; +export { CSVParser, type CSVParserOptions, type StepResult, type ChunkResult, type ParseMeta, type ParserHandle, type CSVMeta } from "./parser"; export { CSVRow } from "./row"; export { DataFrame, type DataFrameOptions } from "./dataframe"; export { CSVWriter, ModificationLog, type CSVWriterOptions } from "./writer"; diff --git a/src/ts/parser.ts b/src/ts/parser.ts index 1c25650..229275d 100644 --- a/src/ts/parser.ts +++ b/src/ts/parser.ts @@ -104,6 +104,22 @@ export interface CSVParserOptions> { chunkSize?: number; } +/** Parse metadata (PapaParse-compatible) */ +export interface CSVMeta { + /** Delimiter used for parsing */ + delimiter: string; + /** Line ending used in the data */ + linebreak: string; + /** Whether parsing was aborted */ + aborted: boolean; + /** Whether output was truncated (e.g. by preview limit) */ + truncated: boolean; + /** Header field names (null if no headers) */ + fields: string[] | null; + /** Elapsed parse time in milliseconds */ + elapsedMs: number; +} + /** Input source types */ type InputSource = string | ReadableStream | ArrayBuffer | Uint8Array; @@ -133,6 +149,8 @@ export class CSVParser> private headerRow: string[] | null = null; private startTime: number = 0; private closed: boolean = false; + private aborted: boolean = false; + private truncated: boolean = false; private sourcePath: string | null = null; // Error tracking @@ -335,6 +353,22 @@ export class CSVParser> return this.headerRow; } + /** + * Get parse metadata (PapaParse-compatible). + * Returns information about the delimiter, linebreak, fields, + * and whether parsing was aborted or truncated. + */ + getMeta(): CSVMeta { + return { + delimiter: this.options.delimiter ?? ",", + linebreak: "\n", + aborted: this.aborted, + truncated: this.truncated, + fields: this.headerRow ? [...this.headerRow] : null, + elapsedMs: this.startTime > 0 ? performance.now() - this.startTime : 0, + }; + } + /** * Pause parsing. */ diff --git a/test/unit/meta.test.ts b/test/unit/meta.test.ts new file mode 100644 index 0000000..1b82ae8 --- /dev/null +++ b/test/unit/meta.test.ts @@ -0,0 +1,130 @@ +/** + * Tests for Issue #14: meta output in parse results + */ + +import { describe, test, expect } from "bun:test"; +import { CSVParser } from "../../src/ts/parser"; +import type { CSVMeta } from "../../src/ts/parser"; + +describe("getMeta()", () => { + test("returns delimiter used", () => { + const data = new TextEncoder().encode( + "name,age\nAlice,30\n" + ); + const parser = new CSVParser(data); + for (const _row of parser) { /* consume */ } + + const meta = parser.getMeta(); + expect(meta.delimiter).toBe(","); + parser.close(); + }); + + test("returns custom delimiter", () => { + const data = new TextEncoder().encode( + "name\tage\nAlice\t30\n" + ); + const parser = new CSVParser(data, { delimiter: "\t" }); + for (const _row of parser) { /* consume */ } + + const meta = parser.getMeta(); + expect(meta.delimiter).toBe("\t"); + parser.close(); + }); + + test("returns linebreak", () => { + const data = new TextEncoder().encode( + "name\nAlice\n" + ); + const parser = new CSVParser(data); + const meta = parser.getMeta(); + + expect(meta.linebreak).toBe("\n"); + parser.close(); + }); + + test("returns fields array with header names", () => { + const data = new TextEncoder().encode( + "name,age,city\nAlice,30,NYC\n" + ); + const parser = new CSVParser(data); + const meta = parser.getMeta(); + + expect(meta.fields).toEqual(["name", "age", "city"]); + parser.close(); + }); + + test("returns null fields without headers", () => { + const data = new TextEncoder().encode( + "Alice,30\n" + ); + const parser = new CSVParser(data, { hasHeader: false }); + const meta = parser.getMeta(); + + expect(meta.fields).toBeNull(); + parser.close(); + }); + + test("aborted is false by default", () => { + const data = new TextEncoder().encode( + "name\nAlice\n" + ); + const parser = new CSVParser(data); + for (const _row of parser) { /* consume */ } + + const meta = parser.getMeta(); + expect(meta.aborted).toBe(false); + parser.close(); + }); + + test("truncated is false by default", () => { + const data = new TextEncoder().encode( + "name\nAlice\nBob\n" + ); + const parser = new CSVParser(data); + for (const _row of parser) { /* consume */ } + + const meta = parser.getMeta(); + expect(meta.truncated).toBe(false); + parser.close(); + }); + + test("elapsedMs is a positive number after parsing", () => { + const data = new TextEncoder().encode( + "name,age\nAlice,30\nBob,25\n" + ); + const parser = new CSVParser(data); + for (const _row of parser) { /* consume */ } + + const meta = parser.getMeta(); + expect(meta.elapsedMs).toBeGreaterThanOrEqual(0); + parser.close(); + }); + + test("fields is a copy, not a reference", () => { + const data = new TextEncoder().encode( + "name,age\nAlice,30\n" + ); + const parser = new CSVParser(data); + const meta1 = parser.getMeta(); + const meta2 = parser.getMeta(); + + // Should be equal but not the same reference + expect(meta1.fields).toEqual(meta2.fields); + expect(meta1.fields).not.toBe(meta2.fields); + parser.close(); + }); + + test("meta available before iteration", () => { + const data = new TextEncoder().encode( + "name,age\nAlice,30\n" + ); + const parser = new CSVParser(data); + + // Meta should be available immediately after construction + const meta = parser.getMeta(); + expect(meta.delimiter).toBe(","); + expect(meta.fields).toEqual(["name", "age"]); + expect(meta.aborted).toBe(false); + parser.close(); + }); +});