jtcsv 2.1.0 → 2.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -17
- package/bin/jtcsv.js +1013 -117
- package/csv-to-json.js +385 -311
- package/examples/simple-usage.js +2 -3
- package/index.d.ts +288 -5
- package/index.js +23 -0
- package/json-to-csv.js +130 -89
- package/package.json +47 -19
- package/plugins/README.md +146 -2
- package/plugins/hono/README.md +25 -0
- package/plugins/hono/index.d.ts +12 -0
- package/plugins/hono/index.js +36 -0
- package/plugins/hono/package.json +35 -0
- package/plugins/nestjs/README.md +33 -0
- package/plugins/nestjs/index.d.ts +25 -0
- package/plugins/nestjs/index.js +77 -0
- package/plugins/nestjs/package.json +37 -0
- package/plugins/nuxt/README.md +25 -0
- package/plugins/nuxt/index.js +21 -0
- package/plugins/nuxt/package.json +35 -0
- package/plugins/nuxt/runtime/composables/useJtcsv.js +6 -0
- package/plugins/nuxt/runtime/plugin.js +6 -0
- package/plugins/remix/README.md +26 -0
- package/plugins/remix/index.d.ts +16 -0
- package/plugins/remix/index.js +62 -0
- package/plugins/remix/package.json +35 -0
- package/plugins/sveltekit/README.md +28 -0
- package/plugins/sveltekit/index.d.ts +17 -0
- package/plugins/sveltekit/index.js +54 -0
- package/plugins/sveltekit/package.json +33 -0
- package/plugins/trpc/README.md +22 -0
- package/plugins/trpc/index.d.ts +7 -0
- package/plugins/trpc/index.js +32 -0
- package/plugins/trpc/package.json +34 -0
- package/src/core/delimiter-cache.js +186 -0
- package/src/core/transform-hooks.js +350 -0
- package/src/engines/fast-path-engine.js +829 -340
- package/src/formats/tsv-parser.js +336 -0
- package/src/index-with-plugins.js +36 -14
- package/cli-tui.js +0 -5
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# @jtcsv/remix
|
|
2
|
+
|
|
3
|
+
Remix helpers for JTCSV.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
```bash
|
|
7
|
+
npm install @jtcsv/remix jtcsv
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## Usage
|
|
11
|
+
```typescript
|
|
12
|
+
import { parseFormData, generateCsvResponse } from 'jtcsv/remix';
|
|
13
|
+
|
|
14
|
+
export async function action({ request }) {
|
|
15
|
+
const rows = await parseFormData(request, { delimiter: ',' });
|
|
16
|
+
return { parsed: rows };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function loader() {
|
|
20
|
+
return generateCsvResponse([{ id: 1, name: 'John' }], 'export.csv');
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Notes
|
|
25
|
+
- `parseFormData` looks for a file field named `file` by default.
|
|
26
|
+
- You can override the field name with `{ fieldName: 'upload' }`.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { CsvToJsonOptions, JsonToCsvOptions } from 'jtcsv';
|
|
2
|
+
|
|
3
|
+
export interface RemixCsvParseOptions extends CsvToJsonOptions {
|
|
4
|
+
fieldName?: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function parseFormData(
|
|
8
|
+
request: Request,
|
|
9
|
+
options?: RemixCsvParseOptions
|
|
10
|
+
): Promise<unknown[]>;
|
|
11
|
+
|
|
12
|
+
export function generateCsvResponse(
|
|
13
|
+
data: unknown[] | Record<string, unknown>,
|
|
14
|
+
filename?: string,
|
|
15
|
+
options?: JsonToCsvOptions
|
|
16
|
+
): Response;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
const jtcsv = require('jtcsv');
|
|
2
|
+
|
|
3
|
+
function normalizeFilename(filename) {
|
|
4
|
+
if (!filename || typeof filename !== 'string') {
|
|
5
|
+
return 'export.csv';
|
|
6
|
+
}
|
|
7
|
+
return filename.includes('.') ? filename : `${filename}.csv`;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async function extractCsvText(formData, fieldName) {
|
|
11
|
+
if (formData.has(fieldName)) {
|
|
12
|
+
const value = formData.get(fieldName);
|
|
13
|
+
if (value && typeof value.text === 'function') {
|
|
14
|
+
return await value.text();
|
|
15
|
+
}
|
|
16
|
+
if (value != null) {
|
|
17
|
+
return String(value);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
for (const value of formData.values()) {
|
|
22
|
+
if (value && typeof value.text === 'function') {
|
|
23
|
+
return await value.text();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function parseFormData(request, options = {}) {
|
|
31
|
+
if (!request || typeof request.formData !== 'function') {
|
|
32
|
+
throw new Error('parseFormData expects a Remix Request with formData()');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const { fieldName = 'file', ...csvOptions } = options;
|
|
36
|
+
const formData = await request.formData();
|
|
37
|
+
const csvText = await extractCsvText(formData, fieldName);
|
|
38
|
+
|
|
39
|
+
if (!csvText) {
|
|
40
|
+
throw new Error('No CSV file or field found in form data');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return jtcsv.csvToJson(csvText, csvOptions);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function generateCsvResponse(data, filename = 'export.csv', options = {}) {
|
|
47
|
+
const safeName = normalizeFilename(filename);
|
|
48
|
+
const rows = Array.isArray(data) ? data : [data];
|
|
49
|
+
const csv = jtcsv.jsonToCsv(rows, options);
|
|
50
|
+
|
|
51
|
+
return new Response(csv, {
|
|
52
|
+
headers: {
|
|
53
|
+
'Content-Type': 'text/csv; charset=utf-8',
|
|
54
|
+
'Content-Disposition': `attachment; filename="${safeName}"`
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
module.exports = {
|
|
60
|
+
parseFormData,
|
|
61
|
+
generateCsvResponse
|
|
62
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jtcsv/remix",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Remix helpers for JTCSV (form-data parsing and CSV responses)",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"types": "index.d.ts",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"remix",
|
|
9
|
+
"csv",
|
|
10
|
+
"json",
|
|
11
|
+
"converter",
|
|
12
|
+
"jtcsv",
|
|
13
|
+
"formdata",
|
|
14
|
+
"response"
|
|
15
|
+
],
|
|
16
|
+
"author": "Ruslan Fomenko",
|
|
17
|
+
"license": "MIT",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/Linol-Hamelton/jtcsv.git",
|
|
21
|
+
"directory": "plugins/remix"
|
|
22
|
+
},
|
|
23
|
+
"bugs": {
|
|
24
|
+
"url": "https://github.com/Linol-Hamelton/jtcsv/issues"
|
|
25
|
+
},
|
|
26
|
+
"homepage": "https://github.com/Linol-Hamelton/jtcsv/tree/main/plugins/remix#readme",
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"jtcsv": "^2.1.3"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"index.js",
|
|
32
|
+
"index.d.ts",
|
|
33
|
+
"README.md"
|
|
34
|
+
]
|
|
35
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# @jtcsv/sveltekit
|
|
2
|
+
|
|
3
|
+
SvelteKit helpers for JTCSV.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
```bash
|
|
7
|
+
npm install @jtcsv/sveltekit jtcsv
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## Usage
|
|
11
|
+
```typescript
|
|
12
|
+
import { parseCsv, generateCsv } from 'jtcsv/sveltekit';
|
|
13
|
+
|
|
14
|
+
export const actions = {
|
|
15
|
+
upload: async ({ request }) => {
|
|
16
|
+
const rows = await parseCsv(request, { delimiter: ',' });
|
|
17
|
+
return { success: true, rows };
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export async function GET() {
|
|
22
|
+
return generateCsv([{ id: 1, name: 'John' }], 'export.csv');
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Notes
|
|
27
|
+
- `parseCsv` reads `request.text()` by default.
|
|
28
|
+
- Use `{ formData: true, fieldName: 'file' }` for multipart uploads.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { CsvToJsonOptions, JsonToCsvOptions } from 'jtcsv';
|
|
2
|
+
|
|
3
|
+
export interface SvelteKitCsvParseOptions extends CsvToJsonOptions {
|
|
4
|
+
fieldName?: string;
|
|
5
|
+
formData?: boolean;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function parseCsv(
|
|
9
|
+
request: Request,
|
|
10
|
+
options?: SvelteKitCsvParseOptions
|
|
11
|
+
): Promise<unknown[]>;
|
|
12
|
+
|
|
13
|
+
export function generateCsv(
|
|
14
|
+
data: unknown[] | Record<string, unknown>,
|
|
15
|
+
filename?: string,
|
|
16
|
+
options?: JsonToCsvOptions
|
|
17
|
+
): Response;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
const jtcsv = require('jtcsv');
|
|
2
|
+
|
|
3
|
+
function normalizeFilename(filename) {
|
|
4
|
+
if (!filename || typeof filename !== 'string') {
|
|
5
|
+
return 'export.csv';
|
|
6
|
+
}
|
|
7
|
+
return filename.includes('.') ? filename : `${filename}.csv`;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async function parseCsv(request, options = {}) {
|
|
11
|
+
if (!request || typeof request.text !== 'function') {
|
|
12
|
+
throw new Error('parseCsv expects a Request instance');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const { fieldName = 'file', ...csvOptions } = options;
|
|
16
|
+
const contentType = request.headers?.get?.('content-type') || '';
|
|
17
|
+
let csvText = null;
|
|
18
|
+
|
|
19
|
+
if (options.formData || contentType.includes('multipart/form-data')) {
|
|
20
|
+
const formData = await request.formData();
|
|
21
|
+
const value = formData.get(fieldName) ?? formData.values().next().value;
|
|
22
|
+
if (value && typeof value.text === 'function') {
|
|
23
|
+
csvText = await value.text();
|
|
24
|
+
} else if (value != null) {
|
|
25
|
+
csvText = String(value);
|
|
26
|
+
}
|
|
27
|
+
} else {
|
|
28
|
+
csvText = await request.text();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (!csvText) {
|
|
32
|
+
throw new Error('No CSV payload found in request');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return jtcsv.csvToJson(csvText, csvOptions);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function generateCsv(data, filename = 'export.csv', options = {}) {
|
|
39
|
+
const safeName = normalizeFilename(filename);
|
|
40
|
+
const rows = Array.isArray(data) ? data : [data];
|
|
41
|
+
const csv = jtcsv.jsonToCsv(rows, options);
|
|
42
|
+
|
|
43
|
+
return new Response(csv, {
|
|
44
|
+
headers: {
|
|
45
|
+
'Content-Type': 'text/csv; charset=utf-8',
|
|
46
|
+
'Content-Disposition': `attachment; filename="${safeName}"`
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = {
|
|
52
|
+
parseCsv,
|
|
53
|
+
generateCsv
|
|
54
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jtcsv/sveltekit",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "SvelteKit helpers for JTCSV",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"types": "index.d.ts",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"sveltekit",
|
|
9
|
+
"csv",
|
|
10
|
+
"json",
|
|
11
|
+
"converter",
|
|
12
|
+
"jtcsv"
|
|
13
|
+
],
|
|
14
|
+
"author": "Ruslan Fomenko",
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/Linol-Hamelton/jtcsv.git",
|
|
19
|
+
"directory": "plugins/sveltekit"
|
|
20
|
+
},
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://github.com/Linol-Hamelton/jtcsv/issues"
|
|
23
|
+
},
|
|
24
|
+
"homepage": "https://github.com/Linol-Hamelton/jtcsv/tree/main/plugins/sveltekit#readme",
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"jtcsv": "^2.1.3"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"index.js",
|
|
30
|
+
"index.d.ts",
|
|
31
|
+
"README.md"
|
|
32
|
+
]
|
|
33
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# @jtcsv/trpc
|
|
2
|
+
|
|
3
|
+
tRPC helper for JTCSV.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
```bash
|
|
7
|
+
npm install @jtcsv/trpc jtcsv
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## Usage
|
|
11
|
+
```typescript
|
|
12
|
+
import { initTRPC } from '@trpc/server';
|
|
13
|
+
import { z } from 'zod';
|
|
14
|
+
import { createCsvProcedure } from 'jtcsv/trpc';
|
|
15
|
+
|
|
16
|
+
const t = initTRPC.create();
|
|
17
|
+
|
|
18
|
+
export const router = t.router({
|
|
19
|
+
parseCsv: createCsvProcedure(t, z.string(), { delimiter: ',' })
|
|
20
|
+
.mutation(async ({ input }) => ({ parsed: input }))
|
|
21
|
+
});
|
|
22
|
+
```
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const jtcsv = require('jtcsv');
|
|
2
|
+
|
|
3
|
+
function extractCsvText(input) {
|
|
4
|
+
if (typeof input === 'string') {
|
|
5
|
+
return input;
|
|
6
|
+
}
|
|
7
|
+
if (input && typeof input === 'object' && typeof input.csv === 'string') {
|
|
8
|
+
return input.csv;
|
|
9
|
+
}
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function createCsvProcedure(t, schema, options = {}) {
|
|
14
|
+
if (!t || !t.procedure) {
|
|
15
|
+
throw new Error('createCsvProcedure expects initTRPC instance');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return t.procedure
|
|
19
|
+
.input(schema)
|
|
20
|
+
.use(async ({ input, next }) => {
|
|
21
|
+
const csvText = extractCsvText(input);
|
|
22
|
+
if (!csvText) {
|
|
23
|
+
throw new Error('CSV input must be a string or { csv: string }');
|
|
24
|
+
}
|
|
25
|
+
const parsed = jtcsv.csvToJson(csvText, options);
|
|
26
|
+
return next({ input: parsed });
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
module.exports = {
|
|
31
|
+
createCsvProcedure
|
|
32
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jtcsv/trpc",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "tRPC helper for JTCSV CSV parsing",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"types": "index.d.ts",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"trpc",
|
|
9
|
+
"csv",
|
|
10
|
+
"json",
|
|
11
|
+
"converter",
|
|
12
|
+
"jtcsv"
|
|
13
|
+
],
|
|
14
|
+
"author": "Ruslan Fomenko",
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/Linol-Hamelton/jtcsv.git",
|
|
19
|
+
"directory": "plugins/trpc"
|
|
20
|
+
},
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://github.com/Linol-Hamelton/jtcsv/issues"
|
|
23
|
+
},
|
|
24
|
+
"homepage": "https://github.com/Linol-Hamelton/jtcsv/tree/main/plugins/trpc#readme",
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"@trpc/server": "^10.0.0",
|
|
27
|
+
"jtcsv": "^2.1.3"
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"index.js",
|
|
31
|
+
"index.d.ts",
|
|
32
|
+
"README.md"
|
|
33
|
+
]
|
|
34
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Кэширование результатов авто-детектирования разделителя
|
|
3
|
+
* Использует WeakMap и LRU кэш для оптимизации производительности
|
|
4
|
+
*
|
|
5
|
+
* @version 1.0.0
|
|
6
|
+
* @date 2026-01-23
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
class DelimiterCache {
|
|
10
|
+
constructor(maxSize = 100) {
|
|
11
|
+
this.weakMap = new WeakMap();
|
|
12
|
+
this.lruCache = new Map();
|
|
13
|
+
this.maxSize = maxSize;
|
|
14
|
+
this.stats = {
|
|
15
|
+
hits: 0,
|
|
16
|
+
misses: 0,
|
|
17
|
+
evictions: 0,
|
|
18
|
+
size: 0
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Генерирует ключ кэша на основе строки и кандидатов
|
|
24
|
+
* @private
|
|
25
|
+
*/
|
|
26
|
+
_generateKey(csv, candidates) {
|
|
27
|
+
// Используем хэш первых 1000 символов для производительности
|
|
28
|
+
const sample = csv.substring(0, Math.min(1000, csv.length));
|
|
29
|
+
const candidatesKey = candidates.join(',');
|
|
30
|
+
return `${this._hashString(sample)}:${candidatesKey}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Простая хэш-функция для строк
|
|
35
|
+
* @private
|
|
36
|
+
*/
|
|
37
|
+
_hashString(str) {
|
|
38
|
+
let hash = 0;
|
|
39
|
+
for (let i = 0; i < str.length; i++) {
|
|
40
|
+
const char = str.charCodeAt(i);
|
|
41
|
+
hash = ((hash << 5) - hash) + char;
|
|
42
|
+
hash = hash & hash; // Convert to 32bit integer
|
|
43
|
+
}
|
|
44
|
+
return hash.toString(36);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Получает значение из кэша
|
|
49
|
+
* @param {string} csv - CSV строка
|
|
50
|
+
* @param {Array} candidates - Кандидаты разделителей
|
|
51
|
+
* @returns {string|null} Кэшированный разделитель или null
|
|
52
|
+
*/
|
|
53
|
+
get(csv, candidates) {
|
|
54
|
+
const key = this._generateKey(csv, candidates);
|
|
55
|
+
|
|
56
|
+
// Проверяем LRU кэш
|
|
57
|
+
if (this.lruCache.has(key)) {
|
|
58
|
+
// Обновляем позицию в LRU
|
|
59
|
+
const value = this.lruCache.get(key);
|
|
60
|
+
this.lruCache.delete(key);
|
|
61
|
+
this.lruCache.set(key, value);
|
|
62
|
+
this.stats.hits++;
|
|
63
|
+
return value;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
this.stats.misses++;
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Сохраняет значение в кэш
|
|
72
|
+
* @param {string} csv - CSV строка
|
|
73
|
+
* @param {Array} candidates - Кандидаты разделителей
|
|
74
|
+
* @param {string} delimiter - Найденный разделитель
|
|
75
|
+
*/
|
|
76
|
+
set(csv, candidates, delimiter) {
|
|
77
|
+
const key = this._generateKey(csv, candidates);
|
|
78
|
+
|
|
79
|
+
// Проверяем необходимость вытеснения из LRU кэша
|
|
80
|
+
if (this.lruCache.size >= this.maxSize) {
|
|
81
|
+
// Удаляем самый старый элемент (первый в Map)
|
|
82
|
+
const firstKey = this.lruCache.keys().next().value;
|
|
83
|
+
this.lruCache.delete(firstKey);
|
|
84
|
+
this.stats.evictions++;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Сохраняем в LRU кэш
|
|
88
|
+
this.lruCache.set(key, delimiter);
|
|
89
|
+
this.stats.size = this.lruCache.size;
|
|
90
|
+
|
|
91
|
+
// Также сохраняем в WeakMap если csv является объектом
|
|
92
|
+
if (typeof csv === 'object' && csv !== null) {
|
|
93
|
+
this.weakMap.set(csv, { candidates, delimiter });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Очищает кэш
|
|
99
|
+
*/
|
|
100
|
+
clear() {
|
|
101
|
+
this.lruCache.clear();
|
|
102
|
+
this.weakMap = new WeakMap();
|
|
103
|
+
this.stats = {
|
|
104
|
+
hits: 0,
|
|
105
|
+
misses: 0,
|
|
106
|
+
evictions: 0,
|
|
107
|
+
size: 0
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Возвращает статистику кэша
|
|
113
|
+
* @returns {Object} Статистика
|
|
114
|
+
*/
|
|
115
|
+
getStats() {
|
|
116
|
+
const total = this.stats.hits + this.stats.misses;
|
|
117
|
+
return {
|
|
118
|
+
...this.stats,
|
|
119
|
+
hitRate: total > 0 ? (this.stats.hits / total) * 100 : 0,
|
|
120
|
+
totalRequests: total
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Оптимизированная версия autoDetectDelimiter с кэшированием
|
|
126
|
+
* @param {string} csv - CSV строка
|
|
127
|
+
* @param {Array} candidates - Кандидаты разделителей
|
|
128
|
+
* @param {DelimiterCache} cache - Экземпляр кэша (опционально)
|
|
129
|
+
* @returns {string} Найденный разделитель
|
|
130
|
+
*/
|
|
131
|
+
static autoDetectDelimiter(csv, candidates = [';', ',', '\t', '|'], cache = null) {
|
|
132
|
+
if (!csv || typeof csv !== 'string') {
|
|
133
|
+
return ';';
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Проверяем кэш если он предоставлен
|
|
137
|
+
if (cache) {
|
|
138
|
+
const cached = cache.get(csv, candidates);
|
|
139
|
+
if (cached !== null) {
|
|
140
|
+
return cached;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const lines = csv.split('\n').filter(line => line.trim().length > 0);
|
|
145
|
+
|
|
146
|
+
if (lines.length === 0) {
|
|
147
|
+
return ';';
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Используем первую непустую строку для детектирования
|
|
151
|
+
const firstLine = lines[0];
|
|
152
|
+
|
|
153
|
+
const counts = {};
|
|
154
|
+
candidates.forEach(delim => {
|
|
155
|
+
const escapedDelim = delim.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
156
|
+
const regex = new RegExp(escapedDelim, 'g');
|
|
157
|
+
const matches = firstLine.match(regex);
|
|
158
|
+
counts[delim] = matches ? matches.length : 0;
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// Находим разделитель с максимальным количеством
|
|
162
|
+
let maxCount = -1;
|
|
163
|
+
let detectedDelimiter = ';';
|
|
164
|
+
|
|
165
|
+
for (const [delim, count] of Object.entries(counts)) {
|
|
166
|
+
if (count > maxCount) {
|
|
167
|
+
maxCount = count;
|
|
168
|
+
detectedDelimiter = delim;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Если разделитель не найден или есть ничья, возвращаем стандартный
|
|
173
|
+
if (maxCount === 0) {
|
|
174
|
+
detectedDelimiter = ';';
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Сохраняем в кэш если он предоставлен
|
|
178
|
+
if (cache) {
|
|
179
|
+
cache.set(csv, candidates, detectedDelimiter);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return detectedDelimiter;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
module.exports = DelimiterCache;
|