diff --git a/.changeset/quick-hats-tease.md b/.changeset/quick-hats-tease.md new file mode 100644 index 00000000..1315c8ee --- /dev/null +++ b/.changeset/quick-hats-tease.md @@ -0,0 +1,5 @@ +--- +"web-csv-toolbox": minor +--- + +Dynamic Type Inference and User-Defined Types from CSV Headers diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 00000000..a76ca508 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,2 @@ +ignore: + - "**/*.test-d.ts" diff --git a/src/Lexer.ts b/src/Lexer.ts index a37cf682..558b6fd8 100644 --- a/src/Lexer.ts +++ b/src/Lexer.ts @@ -8,7 +8,7 @@ import type { RecordDelimiterToken, Token, } from "./common/types.ts"; -import { COMMA, CRLF, DOUBLE_QUOTE, LF } from "./constants.ts"; +import { CRLF, DEFAULT_DELIMITER, DEFAULT_QUOTATION, LF } from "./constants.ts"; import { escapeRegExp } from "./utils/escapeRegExp.ts"; /** @@ -16,7 +16,10 @@ import { escapeRegExp } from "./utils/escapeRegExp.ts"; * * Lexter tokenizes CSV data into fields and records. */ -export class Lexer { +export class Lexer< + Delimiter extends string = DEFAULT_DELIMITER, + Quotation extends string = DEFAULT_QUOTATION, +> { #delimiter: string; #quotation: string; #buffer = ""; @@ -37,11 +40,14 @@ export class Lexer { * Constructs a new Lexer instance. * @param options - The common options for the lexer. */ - constructor({ - delimiter = COMMA, - quotation = DOUBLE_QUOTE, - signal, - }: CommonOptions & AbortSignalOptions = {}) { + constructor( + options: CommonOptions & AbortSignalOptions = {}, + ) { + const { + delimiter = DEFAULT_DELIMITER, + quotation = DEFAULT_QUOTATION, + signal, + } = options; assertCommonOptions({ delimiter, quotation }); this.#delimiter = delimiter; this.#quotation = quotation; diff --git a/src/LexerTransformer.ts b/src/LexerTransformer.ts index 4da18a4a..b165bd6a 100644 --- a/src/LexerTransformer.ts +++ b/src/LexerTransformer.ts @@ -1,5 +1,6 @@ import { Lexer } from "./Lexer.ts"; import type { CommonOptions, Token } from "./common/types.ts"; +import type { DEFAULT_DELIMITER, DEFAULT_QUOTATION } from "./constants.ts"; /** * A transform stream that converts a stream of tokens into a stream of rows. @@ -31,9 +32,12 @@ import type { CommonOptions, Token } from "./common/types.ts"; * // { type: RecordDelimiter, value: "\r\n", location: {...} } * ``` */ -export class LexerTransformer extends TransformStream { - public readonly lexer: Lexer; - constructor(options: CommonOptions = {}) { +export class LexerTransformer< + Delimiter extends string = DEFAULT_DELIMITER, + Quotation extends string = DEFAULT_QUOTATION, +> extends TransformStream { + public readonly lexer: Lexer; + constructor(options: CommonOptions = {}) { super({ transform: (chunk, controller) => { if (chunk.length !== 0) { diff --git a/src/assertCommonOptions.ts b/src/assertCommonOptions.ts index 20f62d48..ede45ad3 100644 --- a/src/assertCommonOptions.ts +++ b/src/assertCommonOptions.ts @@ -48,12 +48,16 @@ function assertOptionValue( * @throws {RangeError} If any required property is missing or if the delimiter is the same as the quotation. * @throws {TypeError} If any required property is not a string. */ -export function assertCommonOptions( - options: Required, -): asserts options is Required { +export function assertCommonOptions< + Delimiter extends string, + Quotation extends string, +>( + options: Required>, +): asserts options is Required> { for (const name of ["delimiter", "quotation"] as const) { assertOptionValue(options[name], name); } + // @ts-ignore: TS doesn't understand that the values are strings if (options.delimiter === options.quotation) { throw new RangeError( "delimiter must not be the same as quotation, use different characters", diff --git a/src/common/types.ts b/src/common/types.ts index 76287e0b..5653ac74 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -1,3 +1,5 @@ +import type { DEFAULT_DELIMITER, DEFAULT_QUOTATION } from "../constants.ts"; +import type { Join } from "../utils/types.ts"; import type { Field, FieldDelimiter, RecordDelimiter } from "./constants.ts"; /** @@ -138,7 +140,10 @@ export interface AbortSignalOptions { * CSV Common Options. * @category Types */ -export interface CommonOptions { +export interface CommonOptions< + Delimiter extends string, + Quotation extends string, +> { /** * CSV field delimiter. * If you want to parse TSV, specify `'\t'`. @@ -154,13 +159,13 @@ export interface CommonOptions { * * @default ',' */ - delimiter?: string; + delimiter?: Delimiter; /** * CSV field quotation. * * @default '"' */ - quotation?: string; + quotation?: Quotation; } /** @@ -249,8 +254,11 @@ export interface RecordAssemblerOptions
> * Parse options for CSV string. * @category Types */ -export interface ParseOptions
> - extends CommonOptions, +export interface ParseOptions< + Header extends ReadonlyArray = ReadonlyArray, + Delimiter extends string = DEFAULT_DELIMITER, + Quotation extends string = DEFAULT_QUOTATION, +> extends CommonOptions, RecordAssemblerOptions
, AbortSignalOptions {} @@ -285,7 +293,15 @@ export type CSVRecord
> = Record< * * @category Types */ -export type CSVString = string | ReadableStream; +export type CSVString< + Header extends ReadonlyArray = [], + Delimiter extends string = DEFAULT_DELIMITER, + Quotation extends string = DEFAULT_QUOTATION, +> = Header extends readonly [string, ...string[]] + ? + | Join + | ReadableStream> + : string | ReadableStream; /** * CSV Binary. @@ -303,4 +319,10 @@ export type CSVBinary = * * @category Types */ -export type CSV = CSVString | CSVBinary; +export type CSV< + Header extends ReadonlyArray = [], + Delimiter extends string = DEFAULT_DELIMITER, + Quotation extends string = DEFAULT_QUOTATION, +> = Header extends [] + ? CSVString | CSVBinary + : CSVString; diff --git a/src/constants.ts b/src/constants.ts index ce32a5ec..ca558f8b 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,6 +1,13 @@ export const CR = "\r"; +export type CR = typeof CR; + export const CRLF = "\r\n"; +export type CRLF = typeof CRLF; + export const LF = "\n"; +export type LF = typeof LF; + +export type Newline = CRLF | CR | LF; /** * COMMA is a symbol for comma(,). @@ -11,3 +18,9 @@ export const COMMA = ","; * DOUBLE_QUOTE is a symbol for double quote("). */ export const DOUBLE_QUOTE = '"'; + +export const DEFAULT_DELIMITER = COMMA; +export type DEFAULT_DELIMITER = typeof DEFAULT_DELIMITER; + +export const DEFAULT_QUOTATION = DOUBLE_QUOTE; +export type DEFAULT_QUOTATION = typeof DEFAULT_QUOTATION; diff --git a/src/escapeField.ts b/src/escapeField.ts index 961d520f..cde1a928 100644 --- a/src/escapeField.ts +++ b/src/escapeField.ts @@ -1,9 +1,12 @@ import type { assertCommonOptions } from "./assertCommonOptions.ts"; import type { CommonOptions } from "./common/types.ts"; -import { COMMA, DOUBLE_QUOTE } from "./constants.ts"; +import { DEFAULT_DELIMITER, DEFAULT_QUOTATION } from "./constants.ts"; import { occurrences } from "./utils/occurrences.ts"; -export interface EscapeFieldOptions extends CommonOptions { +export interface EscapeFieldOptions< + Delimiter extends string, + Quotation extends string, +> extends CommonOptions { quote?: true; } @@ -17,14 +20,18 @@ const REPLACED_PATTERN_CACHE = new Map(); * @param options The options. * @returns The escaped field. */ -export function escapeField( +export function escapeField< + const Delimiter extends string, + const Quotation extends string, +>( value: string, - { - quotation = DOUBLE_QUOTE, - delimiter = COMMA, - quote, - }: EscapeFieldOptions = {}, + options: EscapeFieldOptions = {}, ): string { + const { + delimiter = DEFAULT_DELIMITER, + quotation = DEFAULT_QUOTATION, + quote, + } = options; if (!REPLACED_PATTERN_CACHE.has(quotation)) { REPLACED_PATTERN_CACHE.set( quotation, diff --git a/src/parse.test-d.ts b/src/parse.test-d.ts new file mode 100644 index 00000000..50f6eb6b --- /dev/null +++ b/src/parse.test-d.ts @@ -0,0 +1,204 @@ +import { describe, expectTypeOf, it } from "vitest"; +import { parse } from "./parse.ts"; +import type { + CSV, + CSVBinary, + CSVRecord, + CSVString, + ParseOptions, +} from "./web-csv-toolbox.ts"; + +describe("parse function", () => { + it("parse should be a function with expected parameter types", () => { + expectTypeOf(parse).toBeFunction(); + expectTypeOf(parse).parameter(0).toMatchTypeOf(); + expectTypeOf(parse) + .parameter(1) + .toMatchTypeOf | undefined>(); + }); + it("should return AsyncIterableIterator<>", () => { + const result = parse("", { header: ["a", "b"] }); + expectTypeOf().toEqualTypeOf< + AsyncIterableIterator> + >(); + }); +}); + +describe("binary parsing", () => { + it("should CSV header of the parsed result will be string array", () => { + expectTypeOf(parse({} as CSVBinary)).toEqualTypeOf< + AsyncIterableIterator> + >(); + }); +}); + +describe("string parsing", () => { + it("should CSV header of the parsed result will be string array", () => { + expectTypeOf(parse("" as string)).toEqualTypeOf< + AsyncIterableIterator> + >(); + + expectTypeOf(parse({} as ReadableStream)).toEqualTypeOf< + AsyncIterableIterator> + >(); + + expectTypeOf(parse({} as ReadableStream)).toEqualTypeOf< + AsyncIterableIterator> + >(); + + expectTypeOf(parse("" as CSVString)).toEqualTypeOf< + AsyncIterableIterator> + >(); + }); +}); + +describe("csv literal string parsing", () => { + const csv1 = `name,age,city,zip +Alice,24,New York,10001 +Bob,36,Los Angeles,90001`; + + it("should csv header of the parsed result will be header's tuple", () => { + expectTypeOf(parse(csv1)).toEqualTypeOf< + AsyncIterableIterator> + >(); + + expectTypeOf( + parse("" as CSVString), + ).toEqualTypeOf< + AsyncIterableIterator> + >(); + + expectTypeOf( + parse("" as CSV), + ).toEqualTypeOf< + AsyncIterableIterator> + >(); + + expectTypeOf(parse(new ReadableStream())).toEqualTypeOf< + AsyncIterableIterator> + >(); + }); +}); + +describe("csv literal string parsing with line breaks, quotation, newline", () => { + const csv1 = `$name$*$*ag +e +$*$city$*$z*i +p*$ +Alice*24*New York*$1000 +$1$ +Bob*$36$*$Los$ +Angeles$*90001`; + + it("should csv header of the parsed result will be header's tuple", () => { + expectTypeOf(parse(csv1, { delimiter: "*", quotation: "$" })).toEqualTypeOf< + AsyncIterableIterator< + CSVRecord + > + >(); + + expectTypeOf( + parse("" as CSVString), + ).toEqualTypeOf< + AsyncIterableIterator< + CSVRecord + > + >(); + + expectTypeOf( + parse("" as CSV), + ).toEqualTypeOf< + AsyncIterableIterator< + CSVRecord + > + >(); + + expectTypeOf( + parse(new ReadableStream(), { + delimiter: "*", + quotation: "$", + }), + ).toEqualTypeOf< + AsyncIterableIterator< + CSVRecord + > + >(); + }); +}); + +describe("generics", () => { + it("should CSV header of the parsed result should be the one specified in generics", () => { + expectTypeOf(parse<["name", "age", "city", "zip"]>("")).toEqualTypeOf< + AsyncIterableIterator> + >(); + + expectTypeOf( + parse<["name", "age", "city", "zip"]>({} as CSVBinary), + ).toEqualTypeOf< + AsyncIterableIterator> + >(); + + expectTypeOf( + parse<["name", "age", "city", "zip"]>({} as ReadableStream), + ).toEqualTypeOf< + AsyncIterableIterator> + >(); + + expectTypeOf( + parse<["name", "age", "city", "zip"]>({} as ReadableStream), + ).toEqualTypeOf< + AsyncIterableIterator> + >(); + + expectTypeOf( + parse("" as CSVString), + ).toEqualTypeOf< + AsyncIterableIterator> + >(); + + expectTypeOf( + parse("", { + delimiter: "#", + quotation: "$", + }), + ).toEqualTypeOf< + AsyncIterableIterator> + >(); + + expectTypeOf( + parse( + {} as ReadableStream, + { + delimiter: "#", + quotation: "$", + }, + ), + ).toEqualTypeOf< + AsyncIterableIterator> + >(); + + expectTypeOf( + parse, "#", "$", ["name", "age", "city", "zip"]>( + {} as ReadableStream, + { + delimiter: "#", + quotation: "$", + }, + ), + ).toEqualTypeOf< + AsyncIterableIterator> + >(); + + expectTypeOf( + parse( + "" as CSVString, + { + delimiter: "#", + quotation: "$", + }, + ), + ).toEqualTypeOf< + AsyncIterableIterator> + >(); + }); +}); diff --git a/src/parse.ts b/src/parse.ts index 4d96c775..b4ae98c7 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -6,12 +6,14 @@ import type { ParseBinaryOptions, ParseOptions, } from "./common/types.ts"; +import type { DEFAULT_DELIMITER, DEFAULT_QUOTATION } from "./constants.ts"; import { parseBinary } from "./parseBinary.ts"; import { parseResponse } from "./parseResponse.ts"; import { parseString } from "./parseString.ts"; import { parseStringStream } from "./parseStringStream.ts"; import { parseUint8ArrayStream } from "./parseUint8ArrayStream.ts"; import * as internal from "./utils/convertThisAsyncIterableIteratorToArray.ts"; +import type { PickCSVHeader } from "./utils/types.ts"; /** * Parse CSV to records. @@ -118,9 +120,28 @@ import * as internal from "./utils/convertThisAsyncIterableIteratorToArray.ts"; * // { name: 'Bob', age: '69' } * ``` */ -export function parse
>( +export function parse( + csv: CSVSource, +): AsyncIterableIterator>>; +export function parse>( csv: CSVString, - options?: ParseOptions
, +): AsyncIterableIterator>; +export function parse>( + csv: CSVString, + options: ParseOptions
, +): AsyncIterableIterator>; +export function parse< + const CSVSource extends CSVString, + const Delimiter extends string = DEFAULT_DELIMITER, + const Quotation extends string = DEFAULT_QUOTATION, + const Header extends ReadonlyArray = PickCSVHeader< + CSVSource, + Delimiter, + Quotation + >, +>( + csv: CSVSource, + options: ParseOptions, ): AsyncIterableIterator>; /** * Parse CSV binary to records. @@ -158,11 +179,11 @@ export function parse
>( * } * ``` */ -export function parse
>( +export function parse>( csv: CSVBinary, options?: ParseBinaryOptions
, ): AsyncIterableIterator>; -export async function* parse
>( +export async function* parse>( csv: CSV, options?: ParseBinaryOptions
, ): AsyncIterableIterator> { diff --git a/src/parseString.spec.ts b/src/parseString.spec.ts index c4338d8c..39f5acb9 100644 --- a/src/parseString.spec.ts +++ b/src/parseString.spec.ts @@ -55,7 +55,7 @@ describe("parseString function", () => { it("should throw an error if options is invalid", () => { expect(async () => { - for await (const _ of parseString("", { delimiter: "" })) { + for await (const _ of parseString("", { delimiter: "" as string })) { // Do nothing. } }).rejects.toThrowErrorMatchingInlineSnapshot( diff --git a/src/parseString.test-d.ts b/src/parseString.test-d.ts new file mode 100644 index 00000000..e4048e3e --- /dev/null +++ b/src/parseString.test-d.ts @@ -0,0 +1,63 @@ +import { describe, expectTypeOf, it } from "vitest"; +import { type CSVRecord, parseString } from "./web-csv-toolbox.ts"; + +describe("string parsing", () => { + it("should CSV header of the parsed result will be string array", () => { + expectTypeOf(parseString("" as string)).toEqualTypeOf< + AsyncIterableIterator> + >(); + }); +}); + +describe("csv literal string parsing", () => { + const csv1 = `name,age,city,zip +Alice,24,New York,10001 +Bob,36,Los Angeles,90001`; + + it("should csv header of the parsed result will be header's tuple", () => { + expectTypeOf(parseString(csv1)).toEqualTypeOf< + AsyncIterableIterator> + >(); + }); +}); + +describe("csv literal string parsing with line breaks, quotation, newline", () => { + const csv1 = `$name$*$*ag +e +$*$city$*$z*i +p*$ +Alice*24*New York*$1000 +$1$ +Bob*$36$*$Los$ +Angeles$*90001`; + + it("should csv header of the parsed result will be header's tuple", () => { + expectTypeOf( + parseString(csv1, { delimiter: "*", quotation: "$" }), + ).toEqualTypeOf< + AsyncIterableIterator< + CSVRecord + > + >(); + }); +}); + +describe("generics", () => { + it("should CSV header of the parsed result should be the one specified in generics", () => { + expectTypeOf(parseString<["name", "age", "city", "zip"]>("")).toEqualTypeOf< + AsyncIterableIterator> + >(); + + expectTypeOf( + parseString( + "", + { + delimiter: "#", + quotation: "$", + }, + ), + ).toEqualTypeOf< + AsyncIterableIterator> + >(); + }); +}); diff --git a/src/parseString.ts b/src/parseString.ts index 8e5c5a0b..3f768fe5 100644 --- a/src/parseString.ts +++ b/src/parseString.ts @@ -1,9 +1,11 @@ import type { CSVRecord, ParseOptions } from "./common/types.ts"; import { commonParseErrorHandling } from "./commonParseErrorHandling.ts"; +import type { DEFAULT_DELIMITER, DEFAULT_QUOTATION } from "./constants.ts"; import { parseStringToArraySync } from "./parseStringToArraySync.ts"; import { parseStringToIterableIterator } from "./parseStringToIterableIterator.ts"; import { parseStringToStream } from "./parseStringToStream.ts"; import * as internal from "./utils/convertThisAsyncIterableIteratorToArray.ts"; +import type { PickCSVHeader } from "./utils/types.ts"; /** * Parse CSV string to records. @@ -31,6 +33,33 @@ import * as internal from "./utils/convertThisAsyncIterableIteratorToArray.ts"; * // { name: 'Bob', age: '69' } * ``` */ +export function parseString( + csv: CSVSource, +): AsyncIterableIterator>>; +export function parseString>( + csv: string, +): AsyncIterableIterator>; +export function parseString>( + csv: string, + options: ParseOptions
, +): AsyncIterableIterator>; +export function parseString< + const CSVSource extends string, + const Delimiter extends string = DEFAULT_DELIMITER, + const Quotation extends string = DEFAULT_QUOTATION, + const Header extends ReadonlyArray = PickCSVHeader< + CSVSource, + Delimiter, + Quotation + >, +>( + csv: CSVSource, + options?: ParseOptions, +): AsyncIterableIterator>; +export function parseString( + csv: string, + options?: ParseOptions, +): AsyncIterableIterator>; export async function* parseString
>( csv: string, options?: ParseOptions
, diff --git a/src/parseStringStream.test-d.ts b/src/parseStringStream.test-d.ts new file mode 100644 index 00000000..61f13f50 --- /dev/null +++ b/src/parseStringStream.test-d.ts @@ -0,0 +1,134 @@ +import { describe, expectTypeOf, it } from "vitest"; +import { + type CSVRecord, + type ParseOptions, + parseStringStream, +} from "./web-csv-toolbox.ts"; + +describe("parseStringStream function", () => { + it("parseStringStream should be a function with expected parameter types", () => { + expectTypeOf(parseStringStream).toBeFunction(); + expectTypeOf(parseStringStream) + .parameter(0) + .toMatchTypeOf>(); + expectTypeOf(parseStringStream) + .parameter(1) + .toMatchTypeOf | undefined>(); + }); +}); + +describe("string ReadableStream parsing", () => { + it("should CSV header of the parsed result will be string array", () => { + expectTypeOf(parseStringStream({} as ReadableStream)).toEqualTypeOf< + AsyncIterableIterator> + >(); + + expectTypeOf(parseStringStream({} as ReadableStream)).toEqualTypeOf< + AsyncIterableIterator> + >(); + }); +}); + +describe("csv literal ReadableStream parsing", () => { + const csv1 = `name,age,city,zip +Alice,24,New York,10001 +Bob,36,Los Angeles,90001`; + + it("should csv header of the parsed result will be header's tuple", () => { + expectTypeOf( + parseStringStream(new ReadableStream()), + ).toEqualTypeOf< + AsyncIterableIterator> + >(); + }); +}); + +describe("csv literal ReadableStream parsing with line breaks, quotation, newline", () => { + const csv1 = `$name$*$*ag +e +$*$city$*$z*i +p*$ +Alice*24*New York*$1000 +$1$ +Bob*$36$*$Los$ +Angeles$*90001`; + + it("should csv header of the parsed result will be header's tuple", () => { + expectTypeOf( + parseStringStream(new ReadableStream(), { + delimiter: "*", + quotation: "$", + }), + ).toEqualTypeOf< + AsyncIterableIterator< + CSVRecord + > + >(); + }); +}); + +describe("generics", () => { + it("should CSV header of the parsed result should be the one specified in generics", () => { + expectTypeOf( + parseStringStream( + {} as ReadableStream, + ), + ).toEqualTypeOf< + AsyncIterableIterator> + >(); + + expectTypeOf( + parseStringStream( + {} as ReadableStream, + ), + ).toEqualTypeOf< + AsyncIterableIterator> + >(); + + expectTypeOf( + parseStringStream< + ReadableStream, + readonly ["name", "age", "city", "zip"] + >({} as ReadableStream), + ).toEqualTypeOf< + AsyncIterableIterator> + >(); + + expectTypeOf( + parseStringStream< + ReadableStream, + readonly ["name", "age", "city", "zip"] + >({} as ReadableStream), + ).toEqualTypeOf< + AsyncIterableIterator> + >(); + + expectTypeOf( + parseStringStream< + ReadableStream, + "#", + "$", + readonly ["name", "age", "city", "zip"] + >({} as ReadableStream, { + delimiter: "#", + quotation: "$", + }), + ).toEqualTypeOf< + AsyncIterableIterator> + >(); + + expectTypeOf( + parseStringStream< + ReadableStream, + "#", + "$", + readonly ["name", "age", "city", "zip"] + >({} as ReadableStream, { + delimiter: "#", + quotation: "$", + }), + ).toEqualTypeOf< + AsyncIterableIterator> + >(); + }); +}); diff --git a/src/parseStringStream.ts b/src/parseStringStream.ts index 8e007df4..5d1b566f 100644 --- a/src/parseStringStream.ts +++ b/src/parseStringStream.ts @@ -1,7 +1,9 @@ import type { CSVRecord, ParseOptions } from "./common/types.ts"; +import type { DEFAULT_DELIMITER, DEFAULT_QUOTATION } from "./constants.ts"; import { parseStringStreamToStream } from "./parseStringStreamToStream.ts"; import { convertStreamToAsyncIterableIterator } from "./utils/convertStreamToAsyncIterableIterator.ts"; import * as internal from "./utils/convertThisAsyncIterableIteratorToArray.ts"; +import type { PickCSVHeader } from "./utils/types.ts"; /** * Parse CSV string stream to records. @@ -37,6 +39,30 @@ import * as internal from "./utils/convertThisAsyncIterableIteratorToArray.ts"; * // { name: 'Bob', age: '69' } * ``` */ +export function parseStringStream< + const CSVSource extends ReadableStream, + const Delimiter extends string = DEFAULT_DELIMITER, + const Quotation extends string = DEFAULT_QUOTATION, + const Header extends ReadonlyArray = PickCSVHeader< + CSVSource, + Delimiter, + Quotation + >, +>( + csv: CSVSource, + options: ParseOptions, +): AsyncIterableIterator>; +export function parseStringStream< + const CSVSource extends ReadableStream, + const Header extends ReadonlyArray = PickCSVHeader, +>( + csv: CSVSource, + options?: ParseOptions
, +): AsyncIterableIterator>; +export function parseStringStream>( + stream: ReadableStream, + options?: ParseOptions
, +): AsyncIterableIterator>; export function parseStringStream
>( stream: ReadableStream, options?: ParseOptions
, diff --git a/src/parseStringStreamToStream.test-d.ts b/src/parseStringStreamToStream.test-d.ts new file mode 100644 index 00000000..04284ef6 --- /dev/null +++ b/src/parseStringStreamToStream.test-d.ts @@ -0,0 +1,131 @@ +import { describe, expectTypeOf, it } from "vitest"; +import { parseStringStreamToStream } from "./parseStringStreamToStream.ts"; +import type { CSVRecord, ParseOptions } from "./web-csv-toolbox.ts"; + +describe("parseStringStreamToStream function", () => { + it("parseStringStreamToStream should be a function with expected parameter types", () => { + expectTypeOf(parseStringStreamToStream).toBeFunction(); + expectTypeOf(parseStringStreamToStream) + .parameter(0) + .toMatchTypeOf(); + expectTypeOf(parseStringStreamToStream) + .parameter(1) + .toMatchTypeOf | undefined>(); + }); +}); + +describe("string ReadableStream parsing", () => { + it("should CSV header of the parsed result will be string array", () => { + expectTypeOf(parseStringStreamToStream({} as ReadableStream)).toEqualTypeOf< + ReadableStream> + >(); + + expectTypeOf( + parseStringStreamToStream({} as ReadableStream), + ).toEqualTypeOf>>(); + }); +}); + +describe("csv literal ReadableStream parsing", () => { + const csv1 = `name,age,city,zip +Alice,24,New York,10001 +Bob,36,Los Angeles,90001`; + + it("should csv header of the parsed result will be header's tuple", () => { + expectTypeOf( + parseStringStreamToStream(new ReadableStream()), + ).toEqualTypeOf< + ReadableStream> + >(); + }); +}); + +describe("csv literal ReadableStream parsing with line breaks, quotation, newline", () => { + const csv1 = `$name$*$*ag +e +$*$city$*$z*i +p*$ +Alice*24*New York*$1000 +$1$ +Bob*$36$*$Los$ +Angeles$*90001`; + + it("should csv header of the parsed result will be header's tuple", () => { + expectTypeOf( + parseStringStreamToStream(new ReadableStream(), { + delimiter: "*", + quotation: "$", + }), + ).toEqualTypeOf< + ReadableStream< + CSVRecord + > + >(); + }); +}); + +describe("generics", () => { + it("should CSV header of the parsed result should be the one specified in generics", () => { + expectTypeOf( + parseStringStreamToStream( + {} as ReadableStream, + ), + ).toEqualTypeOf< + ReadableStream> + >(); + + expectTypeOf( + parseStringStreamToStream( + {} as ReadableStream, + ), + ).toEqualTypeOf< + ReadableStream> + >(); + + expectTypeOf( + parseStringStreamToStream< + ReadableStream, + readonly ["name", "age", "city", "zip"] + >({} as ReadableStream), + ).toEqualTypeOf< + ReadableStream> + >(); + + expectTypeOf( + parseStringStreamToStream< + ReadableStream, + readonly ["name", "age", "city", "zip"] + >({} as ReadableStream), + ).toEqualTypeOf< + ReadableStream> + >(); + + expectTypeOf( + parseStringStreamToStream< + ReadableStream, + "#", + "$", + readonly ["name", "age", "city", "zip"] + >({} as ReadableStream, { + delimiter: "#", + quotation: "$", + }), + ).toEqualTypeOf< + ReadableStream> + >(); + + expectTypeOf( + parseStringStreamToStream< + ReadableStream, + "#", + "$", + readonly ["name", "age", "city", "zip"] + >({} as ReadableStream, { + delimiter: "#", + quotation: "$", + }), + ).toEqualTypeOf< + ReadableStream> + >(); + }); +}); diff --git a/src/parseStringStreamToStream.ts b/src/parseStringStreamToStream.ts index bdcddf64..eacfc355 100644 --- a/src/parseStringStreamToStream.ts +++ b/src/parseStringStreamToStream.ts @@ -1,9 +1,39 @@ import { LexerTransformer } from "./LexerTransformer.ts"; import { RecordAssemblerTransformer } from "./RecordAssemblerTransformer.ts"; import type { CSVRecord, ParseOptions } from "./common/types.ts"; +import type { DEFAULT_DELIMITER, DEFAULT_QUOTATION } from "./constants.ts"; import { pipeline } from "./utils/pipeline.ts"; +import type { PickCSVHeader } from "./utils/types.ts"; -export function parseStringStreamToStream
>( +export function parseStringStreamToStream< + const CSVSource extends ReadableStream, + const Delimiter extends string = DEFAULT_DELIMITER, + const Quotation extends string = DEFAULT_QUOTATION, + const Header extends ReadonlyArray = PickCSVHeader< + CSVSource, + Delimiter, + Quotation + >, +>( + stream: CSVSource, + options: ParseOptions, +): ReadableStream>; +export function parseStringStreamToStream< + const CSVSource extends ReadableStream, + const Header extends ReadonlyArray = PickCSVHeader, +>( + stream: CSVSource, + options?: ParseOptions
, +): ReadableStream>; +export function parseStringStreamToStream< + const Header extends ReadonlyArray, +>( + stream: ReadableStream, + options?: ParseOptions
, +): ReadableStream>; +export function parseStringStreamToStream< + const Header extends ReadonlyArray, +>( stream: ReadableStream, options?: ParseOptions
, ): ReadableStream> { diff --git a/src/parseStringToArraySync.test-d.ts b/src/parseStringToArraySync.test-d.ts new file mode 100644 index 00000000..ca372e95 --- /dev/null +++ b/src/parseStringToArraySync.test-d.ts @@ -0,0 +1,78 @@ +import { describe, expectTypeOf, it } from "vitest"; +import { parseStringToArraySync } from "./parseStringToArraySync.ts"; +import type { CSVRecord, ParseOptions } from "./web-csv-toolbox.ts"; + +describe("parseStringToArraySync function", () => { + it("parseStringToArraySync should be a function with expected parameter types", () => { + expectTypeOf(parseStringToArraySync).toBeFunction(); + expectTypeOf(parseStringToArraySync).parameter(0).toMatchTypeOf(); + expectTypeOf(parseStringToArraySync) + .parameter(1) + .toMatchTypeOf | undefined>(); + }); +}); + +describe("string parsing", () => { + it("should CSV header of the parsed result will be string array", () => { + expectTypeOf(parseStringToArraySync("" as string)).toEqualTypeOf< + CSVRecord[] + >(); + }); +}); + +describe("csv literal string parsing", () => { + const csv1 = `name,age,city,zip +Alice,24,New York,10001 +Bob,36,Los Angeles,90001`; + + it("should csv header of the parsed result will be header's tuple", () => { + expectTypeOf(parseStringToArraySync(csv1)).toMatchTypeOf< + CSVRecord<["name", "age", "city", "zip"]>[] + >(); + }); +}); + +describe("csv literal string parsing with line breaks, quotation, newline", () => { + const csv1 = `$name$*$*ag +e +$*$city$*$z*i +p*$ +Alice*24*New York*$1000 +$1$ +Bob*$36$*$Los$ +Angeles$*90001`; + + it("should csv header of the parsed result will be header's tuple", () => { + expectTypeOf( + parseStringToArraySync(csv1, { delimiter: "*", quotation: "$" }), + ).toMatchTypeOf< + CSVRecord[] + >(); + }); +}); + +describe("generics", () => { + it("should CSV header of the parsed result should be the one specified in generics", () => { + expectTypeOf( + parseStringToArraySync(""), + ).toEqualTypeOf[]>(); + + expectTypeOf( + parseStringToArraySync( + "", + ), + ).toEqualTypeOf[]>(); + + expectTypeOf( + parseStringToArraySync< + string, + "#", + "$", + readonly ["name", "age", "city", "zip"] + >("", { + delimiter: "#", + quotation: "$", + }), + ).toEqualTypeOf[]>(); + }); +}); diff --git a/src/parseStringToArraySync.ts b/src/parseStringToArraySync.ts index c3e27f1c..22ae4118 100644 --- a/src/parseStringToArraySync.ts +++ b/src/parseStringToArraySync.ts @@ -2,11 +2,32 @@ import { Lexer } from "./Lexer.ts"; import { RecordAssembler } from "./RecordAssembler.ts"; import type { CSVRecord, ParseOptions } from "./common/types.ts"; import { commonParseErrorHandling } from "./commonParseErrorHandling.ts"; +import type { DEFAULT_DELIMITER, DEFAULT_QUOTATION } from "./constants.ts"; +import type { PickCSVHeader } from "./utils/types.ts"; -export function parseStringToArraySync
>( - csv: string, - options?: ParseOptions
, -): CSVRecord
[] { +export function parseStringToArraySync< + const CSVSource extends string, + const Delimiter extends string = DEFAULT_DELIMITER, + const Quotation extends string = DEFAULT_QUOTATION, + const Header extends ReadonlyArray = PickCSVHeader< + CSVSource, + Delimiter, + Quotation + >, +>( + csv: CSVSource, + options: ParseOptions, +): CSVRecord
[]; +export function parseStringToArraySync< + const CSVSource extends string, + const Header extends ReadonlyArray = PickCSVHeader, +>(csv: CSVSource, options?: ParseOptions
): CSVRecord
[]; +export function parseStringToArraySync< + const Header extends ReadonlyArray, +>(csv: string, options?: ParseOptions
): CSVRecord
[]; +export function parseStringToArraySync< + const Header extends ReadonlyArray, +>(csv: string, options?: ParseOptions
): CSVRecord
[] { try { const lexer = new Lexer(options); const assembler = new RecordAssembler(options); diff --git a/src/parseStringToArraySyncWASM.test-d.ts b/src/parseStringToArraySyncWASM.test-d.ts new file mode 100644 index 00000000..ade3e88e --- /dev/null +++ b/src/parseStringToArraySyncWASM.test-d.ts @@ -0,0 +1,62 @@ +import { describe, expectTypeOf, it } from "vitest"; +import { parseStringToArraySyncWASM } from "./parseStringToArraySyncWASM.ts"; +import type { CSVRecord } from "./web-csv-toolbox.ts"; + +describe("string parsing", () => { + it("should CSV header of the parsed result will be string array", () => { + expectTypeOf(parseStringToArraySyncWASM("" as string)).toEqualTypeOf< + CSVRecord[] + >(); + }); +}); + +describe("csv literal string parsing", () => { + const csv1 = `name,age,city,zip +Alice,24,New York,10001 +Bob,36,Los Angeles,90001`; + + it("should csv header of the parsed result will be header's tuple", () => { + expectTypeOf(parseStringToArraySyncWASM(csv1)).toMatchTypeOf< + CSVRecord<["name", "age", "city", "zip"]>[] + >(); + }); +}); + +describe("csv literal string parsing with line breaks, quotation, newline", () => { + const csv1 = `$name$*$*ag +e +$*$city$*$z*i +p*$ +Alice*24*New York*$1000 +$1$ +Bob*$36$*$Los$ +Angeles$*90001`; + + it("should csv header of the parsed result will be header's tuple", () => { + expectTypeOf( + parseStringToArraySyncWASM(csv1, { delimiter: "*", quotation: "$" }), + ).toMatchTypeOf< + CSVRecord[] + >(); + }); +}); + +describe("generics", () => { + it("should CSV header of the parsed result should be the one specified in generics", () => { + expectTypeOf( + parseStringToArraySyncWASM<["name", "age", "city", "zip"]>(""), + ).toEqualTypeOf[]>(); + + expectTypeOf( + parseStringToArraySyncWASM< + string, + "#", + "$", + ["name", "age", "city", "zip"] + >("", { + delimiter: "#", + quotation: "$", + }), + ).toEqualTypeOf[]>(); + }); +}); diff --git a/src/parseStringToArraySyncWASM.ts b/src/parseStringToArraySyncWASM.ts index 9518762d..d82a2eb7 100644 --- a/src/parseStringToArraySyncWASM.ts +++ b/src/parseStringToArraySyncWASM.ts @@ -1,8 +1,13 @@ import { parseStringToArraySync } from "web-csv-toolbox-wasm"; import { assertCommonOptions } from "./assertCommonOptions.ts"; import type { CSVRecord, CommonOptions } from "./common/types.ts"; -import { COMMA, DOUBLE_QUOTE } from "./constants.ts"; +import { + DEFAULT_DELIMITER, + DEFAULT_QUOTATION, + DOUBLE_QUOTE, +} from "./constants.ts"; import type { loadWASM } from "./loadWASM.ts"; +import type { PickCSVHeader } from "./utils/types.ts"; /** * Parse CSV string to record of arrays. @@ -40,11 +45,46 @@ import type { loadWASM } from "./loadWASM.ts"; * @beta * @throws {RangeError | TypeError} - If provided options are invalid. */ -export function parseStringToArraySyncWASM
( +export function parseStringToArraySyncWASM< + const CSVSource extends string, + const Delimiter extends string = DEFAULT_DELIMITER, + const Quotation extends string = DEFAULT_QUOTATION, + const Header extends ReadonlyArray = PickCSVHeader< + CSVSource, + Delimiter, + Quotation + >, +>( + csv: CSVSource, + options: CommonOptions, +): CSVRecord
[]; +export function parseStringToArraySyncWASM< + const CSVSource extends string, + const Delimiter extends string = DEFAULT_DELIMITER, + const Quotation extends string = DEFAULT_QUOTATION, + const Header extends ReadonlyArray = PickCSVHeader, +>( + csv: CSVSource, + options?: CommonOptions, +): CSVRecord
[]; +export function parseStringToArraySyncWASM< + const Header extends ReadonlyArray, + const Delimiter extends string = DEFAULT_DELIMITER, + const Quotation extends string = DEFAULT_QUOTATION, +>( csv: string, - options: CommonOptions = {}, + options?: CommonOptions, +): CSVRecord
[]; +export function parseStringToArraySyncWASM< + const Header extends readonly string[], + const Delimiter extends string = DEFAULT_DELIMITER, + const Quotation extends string = DEFAULT_QUOTATION, +>( + csv: string, + options: CommonOptions = {}, ): CSVRecord
[] { - const { delimiter = COMMA, quotation = DOUBLE_QUOTE } = options; + const { delimiter = DEFAULT_DELIMITER, quotation = DEFAULT_QUOTATION } = + options; if (typeof delimiter !== "string" || delimiter.length !== 1) { throw new RangeError( "Invalid delimiter, must be a single character on WASM.", diff --git a/src/parseStringToIterableIterator.test-d.ts b/src/parseStringToIterableIterator.test-d.ts new file mode 100644 index 00000000..314e5db8 --- /dev/null +++ b/src/parseStringToIterableIterator.test-d.ts @@ -0,0 +1,86 @@ +import { describe, expectTypeOf, it } from "vitest"; +import { parseStringToIterableIterator } from "./parseStringToIterableIterator.ts"; +import type { CSVRecord, ParseOptions } from "./web-csv-toolbox.ts"; + +describe("parseStringToIterableIterator function", () => { + it("parseStringToIterableIterator should be a function with expected parameter types", () => { + expectTypeOf(parseStringToIterableIterator).toBeFunction(); + expectTypeOf(parseStringToIterableIterator) + .parameter(0) + .toMatchTypeOf(); + expectTypeOf(parseStringToIterableIterator) + .parameter(1) + .toMatchTypeOf | undefined>(); + }); +}); + +describe("string parsing", () => { + it("should CSV header of the parsed result will be string array", () => { + expectTypeOf(parseStringToIterableIterator("" as string)).toEqualTypeOf< + IterableIterator> + >(); + }); +}); + +describe("csv literal string parsing", () => { + const csv1 = `name,age,city,zip +Alice,24,New York,10001 +Bob,36,Los Angeles,90001`; + + it("should csv header of the parsed result will be header's tuple", () => { + expectTypeOf(parseStringToIterableIterator(csv1)).toEqualTypeOf< + IterableIterator> + >(); + }); +}); + +describe("csv literal string parsing with line breaks, quotation, newline", () => { + const csv1 = `$name$*$*ag +e +$*$city$*$z*i +p*$ +Alice*24*New York*$1000 +$1$ +Bob*$36$*$Los$ +Angeles$*90001`; + + it("should csv header of the parsed result will be header's tuple", () => { + expectTypeOf( + parseStringToIterableIterator(csv1, { delimiter: "*", quotation: "$" }), + ).toEqualTypeOf< + IterableIterator< + CSVRecord + > + >(); + }); +}); + +describe("generics", () => { + it("should CSV header of the parsed result should be the one specified in generics", () => { + expectTypeOf( + parseStringToIterableIterator<["name", "age", "city", "zip"]>(""), + ).toEqualTypeOf< + IterableIterator> + >(); + + expectTypeOf( + parseStringToIterableIterator(""), + ).toEqualTypeOf< + IterableIterator> + >(); + + expectTypeOf( + parseStringToIterableIterator< + string, + "#", + "$", + ["name", "age", "city", "zip"] + >("", { + delimiter: "#", + quotation: "$", + }), + ).toEqualTypeOf< + IterableIterator> + >(); + }); +}); diff --git a/src/parseStringToIterableIterator.ts b/src/parseStringToIterableIterator.ts index a914b838..e4429042 100644 --- a/src/parseStringToIterableIterator.ts +++ b/src/parseStringToIterableIterator.ts @@ -2,9 +2,37 @@ import { Lexer } from "./Lexer.ts"; import { RecordAssembler } from "./RecordAssembler.ts"; import type { CSVRecord, ParseOptions } from "./common/types.ts"; import { commonParseErrorHandling } from "./commonParseErrorHandling.ts"; +import type { DEFAULT_DELIMITER, DEFAULT_QUOTATION } from "./constants.ts"; +import type { PickCSVHeader } from "./utils/types.ts"; export function parseStringToIterableIterator< - Header extends ReadonlyArray, + const CSVSource extends string, + const Delimiter extends string = DEFAULT_DELIMITER, + const Quotation extends string = DEFAULT_QUOTATION, + const Header extends ReadonlyArray = PickCSVHeader< + CSVSource, + Delimiter, + Quotation + >, +>( + stream: CSVSource, + options: ParseOptions, +): IterableIterator>; +export function parseStringToIterableIterator< + const CSVSource extends string, + const Header extends ReadonlyArray = PickCSVHeader, +>( + stream: CSVSource, + options?: ParseOptions
, +): IterableIterator>; +export function parseStringToIterableIterator< + const Header extends ReadonlyArray, +>( + stream: string, + options?: ParseOptions
, +): IterableIterator>; +export function parseStringToIterableIterator< + const Header extends ReadonlyArray, >( csv: string, options?: ParseOptions
, diff --git a/src/parseStringToStream.test-d.ts b/src/parseStringToStream.test-d.ts new file mode 100644 index 00000000..cbfdab6d --- /dev/null +++ b/src/parseStringToStream.test-d.ts @@ -0,0 +1,84 @@ +import { describe, expectTypeOf, it } from "vitest"; +import { parseStringToStream } from "./parseStringToStream.ts"; +import type { CSVRecord, ParseOptions } from "./web-csv-toolbox.ts"; + +describe("parseStringToStream function", () => { + it("parseStringToStream should be a function with expected parameter types", () => { + expectTypeOf(parseStringToStream).toBeFunction(); + expectTypeOf(parseStringToStream).parameter(0).toMatchTypeOf(); + expectTypeOf(parseStringToStream) + .parameter(1) + .toMatchTypeOf | undefined>(); + }); +}); + +describe("string parsing", () => { + it("should CSV header of the parsed result will be string array", () => { + expectTypeOf(parseStringToStream("" as string)).toEqualTypeOf< + ReadableStream> + >(); + }); +}); + +describe("csv literal string parsing", () => { + const csv1 = `name,age,city,zip +Alice,24,New York,10001 +Bob,36,Los Angeles,90001`; + + it("should csv header of the parsed result will be header's tuple", () => { + expectTypeOf(parseStringToStream(csv1)).toEqualTypeOf< + ReadableStream> + >(); + }); +}); + +describe("csv literal string parsing with line breaks, quotation, newline", () => { + const csv1 = `$name$*$*ag +e +$*$city$*$z*i +p*$ +Alice*24*New York*$1000 +$1$ +Bob*$36$*$Los$ +Angeles$*90001`; + + it("should csv header of the parsed result will be header's tuple", () => { + expectTypeOf( + parseStringToStream(csv1, { delimiter: "*", quotation: "$" }), + ).toEqualTypeOf< + ReadableStream< + CSVRecord + > + >(); + }); +}); + +describe("generics", () => { + it("should CSV header of the parsed result should be the one specified in generics", () => { + expectTypeOf( + parseStringToStream(""), + ).toEqualTypeOf< + ReadableStream> + >(); + + expectTypeOf( + parseStringToStream(""), + ).toEqualTypeOf< + ReadableStream> + >(); + + expectTypeOf( + parseStringToStream< + string, + "#", + "$", + readonly ["name", "age", "city", "zip"] + >("", { + delimiter: "#", + quotation: "$", + }), + ).toEqualTypeOf< + ReadableStream> + >(); + }); +}); diff --git a/src/parseStringToStream.ts b/src/parseStringToStream.ts index c014875c..e1601c8e 100644 --- a/src/parseStringToStream.ts +++ b/src/parseStringToStream.ts @@ -2,8 +2,34 @@ import { Lexer } from "./Lexer.ts"; import { RecordAssembler } from "./RecordAssembler.ts"; import type { CSVRecord, ParseOptions } from "./common/types.ts"; import { commonParseErrorHandling } from "./commonParseErrorHandling.ts"; +import type { DEFAULT_DELIMITER, DEFAULT_QUOTATION } from "./constants.ts"; +import type { PickCSVHeader } from "./utils/types.ts"; -export function parseStringToStream
>( +export function parseStringToStream< + const CSVSource extends string, + const Delimiter extends string = DEFAULT_DELIMITER, + const Quotation extends string = DEFAULT_QUOTATION, + const Header extends ReadonlyArray = PickCSVHeader< + CSVSource, + Delimiter, + Quotation + >, +>( + stream: CSVSource, + options: ParseOptions, +): ReadableStream>; +export function parseStringToStream< + const CSVSource extends string, + const Header extends ReadonlyArray = PickCSVHeader, +>( + stream: CSVSource, + options?: ParseOptions
, +): ReadableStream>; +export function parseStringToStream>( + stream: string, + options?: ParseOptions
, +): ReadableStream>; +export function parseStringToStream>( csv: string, options?: ParseOptions
, ): ReadableStream> { diff --git a/src/utils/types.test-d.ts b/src/utils/types.test-d.ts new file mode 100644 index 00000000..9d6b4ba0 --- /dev/null +++ b/src/utils/types.test-d.ts @@ -0,0 +1,357 @@ +import { describe, expectTypeOf, it } from "vitest"; +import { CR, CRLF, LF } from "../constants"; +import type { ExtractCSVHeader, Join, PickCSVHeader, Split } from "./types"; + +const case1csv1 = '"na\nme,",age,city,zip'; + +const case1csv2 = `"name","age +","city","zi +p" +Alice,24,New York,10001 +Bob,36,Los Angeles,90001`; + +const case1csv3 = `"na"me","ag\ne +",city,"zi +p"" +Alice,24,New York,10001 +Bob,36,Los Angeles,90001`; + +const case2csv1 = "'na\nme@'@age@city@zip"; + +const case2csv2 = `'name'@'age +'@'city'@'zi +p' +Alice@24@New York@10001 +Bob@36@Los Angeles@90001`; + +const case2csv3 = `'name'@'age + +'@'c +ity'@' +zi +p' +Alice@'24'@'Ne +w York'@'10 +001' +Bob@36@'Los Ange + +les'@'9 +0001'`; + +const case2csv4 = `'name'@'age + +'@'c +ity'@' +zi +p'`; + +const case2csv5 = `'na${CR}me'@'age'@'ci${CR}${CRLF}ty'@'z${LF}ip'${CR}Alice@24@'New${CR}${LF} York'@10001${LF}Bob@'3${CRLF}6'@'Los Angeles'@'90${CRLF}001'`; + +const case2csv6 = `'@name'@'age + +'@'c +@ity@'@' +zi +p' +'Alice@'@'24'@'@Ne +w York'@'10 +00@1' +Bob@36@'Lo@s Ange + +les'@'@9 +0001'`; + +const case2csv7 = `'@name'@'a'g'e + +'@'c +@i''ty@'@' +'zi +p'' +'Al'ic'e@'@''24''@'@Ne +w Yo'r'k'@'10 +00@1' +'Bob'@36@'Lo@s A'nge' + +les'@'@9 +0001'''`; + +const case2csv8 = + "'namdelimitere'delimiteragedelimitercitydelimiterzip\naadelimiterbbdelimiterccdelimiterdd\needelimiterffdelimiterggdelimiterhh"; + +const case2csv9 = + "name@quotationa\ngequotation@city@zip\naa@quotationbb\nquotation@cc@dd\nee@quotationffquotation@gg@hh"; + +describe("Join", () => { + describe("Generate new string by concatenating all of the elements in array", () => { + it("Default", () => { + expectTypeOf>().toEqualTypeOf<"">(); + expectTypeOf< + Join<["name", "age", "city", "zip"]> + >().toEqualTypeOf<"name,age,city,zip">(); + }); + + it("With different delimiter and quotation", () => { + expectTypeOf>().toEqualTypeOf<"">(); + expectTypeOf< + Join<["name", "age", "city", "zip"], "@", "$"> + >().toEqualTypeOf<"name@age@city@zip">(); + }); + + it("Escape newlines and delimiters and quotation", () => { + expectTypeOf>().toEqualTypeOf<"">(); + expectTypeOf< + Join<["name", "a\nge", "ci,ty", 'zi"p']> + >().toEqualTypeOf<'name,"a\nge","ci,ty","zi"p"'>(); + }); + }); +}); + +describe("Split", () => { + describe("Generate a delimiter-separated tuple from a string", () => { + it("Default", () => { + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>().toEqualTypeOf< + readonly ["name", "age", "city", "zip"] + >(); + expectTypeOf>().toEqualTypeOf< + readonly ['na"me', "ag\ne", "city", 'zip"'] + >(); + }); + + it("With different delimiter and quotation", () => { + expectTypeOf>().toEqualTypeOf(); + expectTypeOf< + Split<"$na$me$@$ag\ne$@city@$zip$$", "@", "$"> + >().toEqualTypeOf(); + expectTypeOf< + Split<'"name\r\n"\r\nage\r\ncity\r\nzip', "\r\n"> + >().toEqualTypeOf(); + expectTypeOf< + Split<"namedelimiteragedelimitercitydelimiterzip", "delimiter"> + >().toEqualTypeOf(); + expectTypeOf< + Split<"name,quotationa\ngequotation,city,zip", ",", "quotation"> + >().toEqualTypeOf(); + }); + }); +}); + +describe("ExtractCSVHeader", () => { + describe("Extract a CSV header string from a CSVString", () => { + it("Default", () => { + expectTypeOf>().toEqualTypeOf<"">(); + expectTypeOf>>().toEqualTypeOf<"">(); + + expectTypeOf< + ExtractCSVHeader + >().toEqualTypeOf<'"na\nme,",age,city,zip'>(); + expectTypeOf< + ExtractCSVHeader> + >().toEqualTypeOf<'"na\nme,",age,city,zip'>(); + + expectTypeOf< + ExtractCSVHeader + >().toEqualTypeOf<'"name","age\n","city","zi\np"'>(); + expectTypeOf< + ExtractCSVHeader> + >().toEqualTypeOf<'"name","age\n","city","zi\np"'>(); + + expectTypeOf< + ExtractCSVHeader + >().toEqualTypeOf<'"name","age\n","city","zi\np"'>(); + expectTypeOf< + ExtractCSVHeader> + >().toEqualTypeOf<'"name","age\n","city","zi\np"'>(); + + expectTypeOf< + ExtractCSVHeader + >().toEqualTypeOf<'"na"me","ag\ne\n",city,"zi\np""'>(); + expectTypeOf< + ExtractCSVHeader> + >().toEqualTypeOf<'"na"me","ag\ne\n",city,"zi\np""'>(); + }); + + it("With different delimiter and quotation", () => { + expectTypeOf>().toEqualTypeOf<"">(); + expectTypeOf< + ExtractCSVHeader, "@", "$"> + >().toEqualTypeOf<"">(); + + expectTypeOf< + ExtractCSVHeader + >().toEqualTypeOf<"'na\nme@'@age@city@zip">(); + expectTypeOf< + ExtractCSVHeader, "@", "'"> + >().toEqualTypeOf<"'na\nme@'@age@city@zip">(); + + expectTypeOf< + ExtractCSVHeader + >().toEqualTypeOf<"'name'@'age\n'@'city'@'zi\np'">(); + expectTypeOf< + ExtractCSVHeader, "@", "'"> + >().toEqualTypeOf<"'name'@'age\n'@'city'@'zi\np'">(); + + expectTypeOf< + ExtractCSVHeader + >().toEqualTypeOf<"'name'@'age\n\n'@'c\nity'@'\nzi\np'">(); + expectTypeOf< + ExtractCSVHeader, "@", "'"> + >().toEqualTypeOf<"'name'@'age\n\n'@'c\nity'@'\nzi\np'">(); + + expectTypeOf< + ExtractCSVHeader + >().toEqualTypeOf<"'name'@'age\n\n'@'c\nity'@'\nzi\np'">(); + expectTypeOf< + ExtractCSVHeader, "@", "'"> + >().toEqualTypeOf<"'name'@'age\n\n'@'c\nity'@'\nzi\np'">(); + + expectTypeOf< + ExtractCSVHeader + >().toEqualTypeOf<"'na\rme'@'age'@'ci\r\r\nty'@'z\nip'">(); + expectTypeOf< + ExtractCSVHeader, "@", "'"> + >().toEqualTypeOf<"'na\rme'@'age'@'ci\r\r\nty'@'z\nip'">(); + + expectTypeOf< + ExtractCSVHeader + >().toEqualTypeOf<"'@name'@'age\n\n'@'c\n@ity@'@'\nzi\np'">(); + expectTypeOf< + ExtractCSVHeader, "@", "'"> + >().toEqualTypeOf<"'@name'@'age\n\n'@'c\n@ity@'@'\nzi\np'">(); + + expectTypeOf< + ExtractCSVHeader + >().toEqualTypeOf<"'@name'@'a'g'e\n\n'@'c\n@i''ty@'@'\n'zi\np''">(); + expectTypeOf< + ExtractCSVHeader, "@", "'"> + >().toEqualTypeOf<"'@name'@'a'g'e\n\n'@'c\n@i''ty@'@'\n'zi\np''">(); + + expectTypeOf< + ExtractCSVHeader + >().toEqualTypeOf<"'namdelimitere'delimiteragedelimitercitydelimiterzip">(); + expectTypeOf< + ExtractCSVHeader, "delimiter", "'"> + >().toEqualTypeOf<"'namdelimitere'delimiteragedelimitercitydelimiterzip">(); + + expectTypeOf< + ExtractCSVHeader + >().toEqualTypeOf<"name@quotationa\ngequotation@city@zip">(); + expectTypeOf< + ExtractCSVHeader, "@", "quotation"> + >().toEqualTypeOf<"name@quotationa\ngequotation@city@zip">(); + }); + }); +}); + +describe("PickCSVHeader", () => { + describe("Generates a delimiter-separated tuple of CSV headers from a CSVString", () => { + it("Default", () => { + expectTypeOf>().toEqualTypeOf(); + expectTypeOf>>().toEqualTypeOf< + readonly string[] + >(); + + expectTypeOf>().toEqualTypeOf< + readonly ["na\nme,", "age", "city", "zip"] + >(); + expectTypeOf< + PickCSVHeader> + >().toEqualTypeOf(); + + expectTypeOf>().toEqualTypeOf< + readonly ["name", "age\n", "city", "zi\np"] + >(); + expectTypeOf< + PickCSVHeader> + >().toEqualTypeOf(); + + expectTypeOf>().toEqualTypeOf< + readonly ["name", "age\n", "city", "zi\np"] + >(); + expectTypeOf< + PickCSVHeader> + >().toEqualTypeOf(); + + expectTypeOf>().toEqualTypeOf< + readonly ['na"me', "ag\ne\n", "city", 'zi\np"'] + >(); + expectTypeOf< + PickCSVHeader> + >().toEqualTypeOf(); + }); + + it("With different delimiter and quotation", () => { + expectTypeOf>().toEqualTypeOf< + readonly string[] + >(); + expectTypeOf, "@", "$">>().toEqualTypeOf< + readonly string[] + >(); + + expectTypeOf>().toEqualTypeOf< + readonly ["na\nme@", "age", "city", "zip"] + >(); + expectTypeOf< + PickCSVHeader, "@", "'"> + >().toEqualTypeOf(); + + expectTypeOf>().toEqualTypeOf< + readonly ["name", "age\n", "city", "zi\np"] + >(); + expectTypeOf< + PickCSVHeader, "@", "'"> + >().toEqualTypeOf(); + + expectTypeOf>().toEqualTypeOf< + readonly ["name", "age\n\n", "c\nity", "\nzi\np"] + >(); + expectTypeOf< + PickCSVHeader, "@", "'"> + >().toEqualTypeOf(); + + expectTypeOf>().toEqualTypeOf< + readonly ["name", "age\n\n", "c\nity", "\nzi\np"] + >(); + expectTypeOf< + PickCSVHeader, "@", "'"> + >().toEqualTypeOf(); + + expectTypeOf>().toEqualTypeOf< + readonly ["na\rme", "age", "ci\r\r\nty", "z\nip"] + >(); + expectTypeOf< + PickCSVHeader, "@", "'"> + >().toEqualTypeOf(); + + expectTypeOf>().toEqualTypeOf< + readonly ["@name", "age\n\n", "c\n@ity@", "\nzi\np"] + >(); + expectTypeOf< + PickCSVHeader, "@", "'"> + >().toEqualTypeOf(); + + expectTypeOf>().toEqualTypeOf< + readonly ["@name", "a'g'e\n\n", "c\n@i''ty@", "\n'zi\np'"] + >(); + expectTypeOf< + PickCSVHeader, "@", "'"> + >().toEqualTypeOf< + readonly ["@name", "a'g'e\n\n", "c\n@i''ty@", "\n'zi\np'"] + >(); + + expectTypeOf< + PickCSVHeader + >().toEqualTypeOf(); + expectTypeOf< + PickCSVHeader, "delimiter", "'"> + >().toEqualTypeOf(); + + expectTypeOf< + PickCSVHeader + >().toEqualTypeOf(); + expectTypeOf< + PickCSVHeader, "@", "quotation"> + >().toEqualTypeOf(); + }); + }); +}); diff --git a/src/utils/types.ts b/src/utils/types.ts new file mode 100644 index 00000000..0d0eb6b0 --- /dev/null +++ b/src/utils/types.ts @@ -0,0 +1,200 @@ +import type { + DEFAULT_DELIMITER, + DEFAULT_QUOTATION, + Newline, +} from "../constants.ts"; +import type { CSVString } from "../web-csv-toolbox.ts"; + +/** + * Generate new string by concatenating all of the elements in array. + * + * @category Types + * + * @example Default + * + * ```ts + * const header = ["name", "age", "city", "zip"]; + * + * type _ = Join + * // `name,age,city,zip` + * ``` + * + * @example With different delimiter and quotation + * + * ```ts + * const header = ["name", "a\nge", "city", "zip"]; + * + * type _ = Join + * // `name@$a\nge$@city@zip` + * ``` + */ +export type Join< + Chars extends ReadonlyArray, + Delimiter extends string = DEFAULT_DELIMITER, + Quotation extends string = DEFAULT_QUOTATION, + Nl extends string = Exclude, +> = Chars extends readonly [infer F, ...infer R] + ? F extends string + ? R extends string[] + ? `${F extends `${string}${Nl | Delimiter | Quotation}${string}` + ? `${Quotation}${F}${Quotation}` + : F}${R extends [] ? "" : Delimiter}${Join}` + : string + : string + : ""; + +/** + * Generate a delimiter-separated tuple from a string. + * + * @category Types + * + * @example Default + * + * ```ts + * const header = `name,age,city,zip`; + * + * type _ = Split + * // ["name", "age", "city", "zip"] + * ``` + * + * @example With different delimiter and quotation + * + * ```ts + * const header = `name@$a + * ge$@city@zip`; + * + * type _ = Split + * // ["name", "a\nge", "city", "zip"] + * ``` + */ +export type Split< + Char extends string, + Delimiter extends string = DEFAULT_DELIMITER, + Quotation extends string = DEFAULT_QUOTATION, + Escaping extends boolean = false, + Col extends string = "", + Result extends string[] = [], +> = Char extends `${Delimiter}${infer R}` + ? Escaping extends true + ? Split + : Split + : Char extends `${Quotation}${infer R}` + ? Escaping extends true + ? R extends "" | Delimiter | `${Delimiter}${string}` + ? Split + : Split + : Split + : Char extends `${infer F}${infer R}` + ? Split + : [...Result, Col] extends [""] + ? readonly string[] + : readonly [...Result, Col]; + +type ExtractString = Source extends + | `${infer S}` + // biome-ignore lint/suspicious/noRedeclare: + | ReadableStream + ? S + : string; + +type ExtractCSVBody< + CSVSource extends CSVString, + Delimiter extends string = DEFAULT_DELIMITER, + Quotation extends string = DEFAULT_QUOTATION, + Nl extends string = Exclude, + Escaping extends boolean = false, +> = ExtractString extends `${Quotation}${infer R}` + ? Escaping extends true + ? R extends Delimiter | Nl | `${Delimiter | Nl}${string}` + ? ExtractCSVBody + : ExtractCSVBody + : ExtractCSVBody + : ExtractString extends `${infer _ extends Nl}${infer R}` + ? Escaping extends true + ? ExtractCSVBody + : R + : ExtractString extends `${infer _}${infer R}` + ? ExtractCSVBody + : ""; + +/** + * Extract a CSV header string from a CSVString. + * + * @category Types + * + * @example Default + * + * ```ts + * const csv = `name,age + * Alice,42 + * Bob,69`; + * + * type _ = ExtractCSVHeader + * // "name,age" + * ``` + * + * @example With different delimiter and quotation + * + * ```ts + * const csv = `name@$a + * ge$ + * $Ali + * ce$@42 + * Bob@69`; + * + * type _ = ExtractCSVHeader + * // "name@$a\nge$" + * ``` + */ +export type ExtractCSVHeader< + CSVSource extends CSVString, + Delimiter extends string = DEFAULT_DELIMITER, + Quotation extends string = DEFAULT_QUOTATION, + Nl extends string = Exclude, + Escaping extends boolean = false, +> = ExtractString extends `${infer Header}${Newline}${ExtractCSVBody< + CSVSource, + Delimiter, + Quotation, + Nl, + Escaping +>}` + ? Header + : ExtractString; + +/** + * Generates a delimiter-separated tuple of CSV headers from a CSVString. + * + * @category Types + * + * @example Default + * + * ```ts + * const csv = `name,age + * Alice,42 + * Bob,69`; + * + * type _ = PickCSVHeader + * // ["name", "age"] + * ``` + * + * @example With different delimiter and quotation + * + * ```ts + * const csv = `name@$a + * ge$ + * $Ali + * ce$@42 + * Bob@69`; + * + * type _ = PickCSVHeader + * // ["name", "a\nge"] + * ``` + */ +export type PickCSVHeader< + CSVSource extends CSVString, + Delimiter extends string = DEFAULT_DELIMITER, + Quotation extends string = DEFAULT_QUOTATION, +> = ExtractString extends `${infer S}` + ? Split, Delimiter, Quotation> + : ReadonlyArray;