@utilix-tech/sdk 0.39.0 → 0.41.0

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.
Files changed (35) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +30 -5
  3. package/dist/{chunk-47RI5V3B.js → chunk-2MTHD2LG.js} +62 -1
  4. package/dist/{chunk-Y4BVU76A.js → chunk-7WSFJTYP.js} +116 -1
  5. package/dist/{chunk-RZUNTYEH.js → chunk-JR354NH4.js} +61 -1
  6. package/dist/{chunk-VEVNIGJ6.js → chunk-PET2CGTH.js} +21 -1
  7. package/dist/{data-BCf3dm-E.d.cts → data-BKpEOcOX.d.cts} +35 -2
  8. package/dist/{data-BCf3dm-E.d.ts → data-BKpEOcOX.d.ts} +35 -2
  9. package/dist/{hashing-Dnj0IAZf.d.cts → hashing-CJjlvqS_.d.cts} +28 -2
  10. package/dist/{hashing-Dnj0IAZf.d.ts → hashing-CJjlvqS_.d.ts} +28 -2
  11. package/dist/index.cjs +256 -0
  12. package/dist/index.d.cts +4 -4
  13. package/dist/index.d.ts +4 -4
  14. package/dist/index.js +4 -4
  15. package/dist/{network-CKPoesdD.d.ts → network-BC8McQOh.d.cts} +60 -2
  16. package/dist/{network-CKPoesdD.d.cts → network-BC8McQOh.d.ts} +60 -2
  17. package/dist/tools/data.cjs +60 -0
  18. package/dist/tools/data.d.cts +1 -1
  19. package/dist/tools/data.d.ts +1 -1
  20. package/dist/tools/data.js +1 -1
  21. package/dist/tools/hashing.cjs +20 -0
  22. package/dist/tools/hashing.d.cts +1 -1
  23. package/dist/tools/hashing.d.ts +1 -1
  24. package/dist/tools/hashing.js +1 -1
  25. package/dist/tools/network.cjs +115 -0
  26. package/dist/tools/network.d.cts +1 -1
  27. package/dist/tools/network.d.ts +1 -1
  28. package/dist/tools/network.js +1 -1
  29. package/dist/tools/units.cjs +61 -0
  30. package/dist/tools/units.d.cts +1 -1
  31. package/dist/tools/units.d.ts +1 -1
  32. package/dist/tools/units.js +1 -1
  33. package/dist/{units-HQrMCiNM.d.ts → units-CzPwRC03.d.cts} +50 -2
  34. package/dist/{units-HQrMCiNM.d.cts → units-CzPwRC03.d.ts} +50 -2
  35. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  Full release notes for all Utilix surfaces (browser tools, REST API, SDKs, MCP server) live at [utilix.tech/changelog](https://utilix.tech/changelog). This file tracks just this package.
4
4
 
5
+ ## 0.41.0
6
+ - Add `buildRangeHeader`, `parseContentRangeHeader` (http-range-header-builder): build an HTTP `Range` request header from explicit, open-ended, or suffix ("last N bytes") byte ranges, and parse a server's `Content-Range` response to verify what it actually sent back.
7
+ - Add `findCsvDuplicateRows` (csv-duplicate-row-finder): group CSV/TSV rows that share the same value across a chosen set of key columns, or the entire row, and report each duplicate group's occurrence count and data row numbers.
8
+
9
+ ## 0.40.0
10
+ - Add `calculateCashBreakdown`, `calculateChange` (cash-till-calculator): break a cash amount down into the minimum count of bills/coins from a set of denominations (largest first), or compute change owed for a payment/tender pair and its breakdown.
11
+ - Add `generateEtag` (etag-generator): compute a content-hash ETag (MD5/SHA-1/SHA-256) for a response header, with an optional weak-validator (W/) prefix.
12
+
5
13
  ## 0.39.0
6
14
  - Add `generateSri`, `generateSriAll` (sri-hash-generator): compute SHA-256/384/512 Subresource Integrity hashes for a script or stylesheet's content, formatted as the `<algorithm>-<base64>` string an HTML `integrity` attribute expects.
7
15
  - Add `analyzeEmailHeaders` (email-header-analyzer): parse a raw email header block into structured From/To/Subject fields, the Received hop chain in order, and SPF/DKIM/DMARC results from an Authentication-Results header.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @utilix-tech/sdk
2
2
 
3
- **610 developer utility functions for Node.js: runs locally, no API key required.**
3
+ **618 developer utility functions for Node.js: runs locally, no API key required.**
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/@utilix-tech/sdk?style=flat-square&color=cb3837)](https://www.npmjs.com/package/@utilix-tech/sdk)
6
6
  [![npm downloads](https://img.shields.io/npm/dm/@utilix-tech/sdk?style=flat-square)](https://www.npmjs.com/package/@utilix-tech/sdk)
@@ -135,6 +135,10 @@ generateHtpasswdFile([{ username: "alice", password: "secret" }]);
135
135
  // Subresource Integrity hashes for a script/stylesheet's content (SHA-256/384/512)
136
136
  generateSriAll("console.log(1)");
137
137
  // [{ algorithm: "SHA-256", base64: "...", integrity: "sha256-..." }, ...]
138
+
139
+ // Content-hash ETag header value (MD5/SHA-1/SHA-256), with an optional weak (W/) prefix
140
+ generateEtag("console.log(1)");
141
+ // { algorithm: "SHA-1", weak: false, hash: "...", etag: "\"...\"", snippet: "ETag: \"...\"" }
138
142
  ```
139
143
 
140
144
  ---
@@ -197,7 +201,7 @@ import { parseCsv, csvToJson, validateYaml, tomlToJson, jsonToToml,
197
201
  formatXml, xmlToJson, jsonToXml, parseIni,
198
202
  validateNdjson, formatNdjson, ndjsonToJsonArray, diffEnvExample,
199
203
  parseSubtitles, convertSubtitles, shiftSubtitles, parseFrontMatter,
200
- generateMarkdownToc, csvToSqlInsert } from "@utilix-tech/sdk/data";
204
+ generateMarkdownToc, csvToSqlInsert, findCsvDuplicateRows } from "@utilix-tech/sdk/data";
201
205
 
202
206
  // CSV
203
207
  const rows = parseCsv("name,age\nalice,30\nbob,25");
@@ -243,6 +247,10 @@ generateMarkdownToc("# Title\n\n## Section One\n\n## Section Two\n");
243
247
  // CSV to SQL INSERT statements, with basic type inference and identifier quoting
244
248
  csvToSqlInsert("name,age\nAlice,30\nBob,25", { tableName: "users" });
245
249
  // { sql: 'INSERT INTO "users" ("name", "age") VALUES (\'Alice\', 30);\n...', rowCount: 2, ... }
250
+
251
+ // Find duplicate rows in CSV/TSV data, by the whole row or a chosen key column set
252
+ findCsvDuplicateRows("name,email\nAlice,a@x.com\nBob,b@x.com\nAlice,a@x.com");
253
+ // { duplicateGroups: [{ key: ["Alice", "a@x.com"], rowNumbers: [1, 3], count: 2 }], duplicateRowCount: 2, ... }
246
254
  ```
247
255
 
248
256
  ---
@@ -406,6 +414,14 @@ calculateRoi({ initialValue: 1000, finalValue: 1610.51, years: 5 });
406
414
  // Unit price comparator: true cost per unit across package options, flags the cheapest
407
415
  compareUnitPrices([{ label: "12-pack", price: 6, quantity: 12 }, { label: "24-pack", price: 10, quantity: 24 }]);
408
416
  // { items: [...], bestLabel: "24-pack" }
417
+
418
+ // Cash till breakdown: minimum count of bills/coins from a set of denominations
419
+ calculateCashBreakdown(53.75, USD_DENOMINATIONS);
420
+ // { breakdown: [{ denomination: 50, count: 1, subtotal: 50 }, ...], totalCount: 7, remainder: 0 }
421
+
422
+ // Change owed for a payment/tender pair, broken down the same way
423
+ calculateChange(4.65, 5.0, USD_DENOMINATIONS);
424
+ // { changeDue: 0.35, breakdown: [{ denomination: 0.25, count: 1, subtotal: 0.25 }, ...], totalCount: 2 }
409
425
  ```
410
426
 
411
427
  ---
@@ -416,7 +432,8 @@ compareUnitPrices([{ label: "12-pack", price: 6, quantity: 12 }, { label: "24-pa
416
432
  import { getStatusCode, isValidIp, isValidIpv4, isValidIpv6,
417
433
  searchStatusCodes, buildGeoUrl, countryCodeToFlag,
418
434
  parseHar, validateSitemap, validateRobotsTxt,
419
- parseUserAgent, buildCacheControl, buildReferrerPolicy, parseIpv6Address, buildLinkHeader, checkSetCookieHeader, analyzeEmailHeaders } from "@utilix-tech/sdk/network";
435
+ parseUserAgent, buildCacheControl, buildReferrerPolicy, parseIpv6Address, buildLinkHeader, checkSetCookieHeader, analyzeEmailHeaders,
436
+ buildRangeHeader, parseContentRangeHeader } from "@utilix-tech/sdk/network";
420
437
 
421
438
  getStatusCode(404);
422
439
  // { code: 404, text: "Not Found", description: "...", category: "Client Error" }
@@ -471,6 +488,14 @@ checkSetCookieHeader("session=abc123; SameSite=None");
471
488
  // Parse a raw email header block: fields, Received hop chain, SPF/DKIM/DMARC results
472
489
  analyzeEmailHeaders(rawHeaderText);
473
490
  // { from: "...", to: "...", receivedHops: [...], hopCount: 2, spf: "pass", dkim: "pass", dmarc: "pass", warnings: [] }
491
+
492
+ // Build a Range request header from explicit, open-ended, or suffix ("last N bytes") byte ranges
493
+ buildRangeHeader([{ start: 0, end: 1023 }], { totalSize: 146515 });
494
+ // { header: "Range: bytes=0-1023", ranges: [...], totalBytesRequested: 1024, warnings: [] }
495
+
496
+ // Parse a Content-Range response header to verify what a server actually sent back
497
+ parseContentRangeHeader("bytes 200-1000/67589");
498
+ // { unit: "bytes", start: 200, end: 1000, total: 67589, byteLength: 801, isUnsatisfiable: false }
474
499
  ```
475
500
 
476
501
  ---
@@ -703,12 +728,12 @@ readVideoInfo(new Uint8Array(videoBytes));
703
728
  |---|---|
704
729
  | `@utilix-tech/sdk/json` | Format, minify, diff, validate, JSONPath, CSV↔JSON, YAML↔JSON, TS types from data or from a JSON Schema |
705
730
  | `@utilix-tech/sdk/encoding` | Base64, URL, HTML entities, Base32/Base58/Base62 encode/decode |
706
- | `@utilix-tech/sdk/hashing` | MD5, SHA-1/256/512, bcrypt, HMAC, .htpasswd |
731
+ | `@utilix-tech/sdk/hashing` | MD5, SHA-1/256/512, bcrypt, HMAC, .htpasswd, SRI hashes, content-hash ETag generation |
707
732
  | `@utilix-tech/sdk/text` | Case convert, slugify, word count, lorem ipsum, line ops, HTML→Markdown, Levenshtein/Damerau string distance, readability scoring, sentence length distribution, emoji lookup |
708
733
  | `@utilix-tech/sdk/data` | CSV, YAML, TOML, XML, INI, NDJSON parse and convert, SRT/WebVTT subtitles, Markdown front matter parser |
709
734
  | `@utilix-tech/sdk/generators` | UUID v4/v7, ULID, passwords, strength check, entropy estimator, fake data |
710
735
  | `@utilix-tech/sdk/time` | Date diff, timezone convert, cron parse, relative time |
711
- | `@utilix-tech/sdk/units` | Bytes, CSS px/rem, number bases, currency format, credit card/IBAN/barcode/ISBN validation, loan calculator, EMI calculator, compound/simple interest calculators, VAT/sales tax calculator, tip/bill split calculator, break-even point calculator, Rule of 72 calculator, savings goal calculator, ROI calculator |
736
+ | `@utilix-tech/sdk/units` | Bytes, CSS px/rem, number bases, currency format, credit card/IBAN/barcode/ISBN validation, loan calculator, EMI calculator, compound/simple interest calculators, VAT/sales tax calculator, tip/bill split calculator, break-even point calculator, Rule of 72 calculator, savings goal calculator, ROI calculator, cash denomination/till calculator |
712
737
  | `@utilix-tech/sdk/network` | HTTP status codes, IP validation, country flags, geolocation URLs, HAR file parsing, User-Agent parsing, IPv6 address compress/expand |
713
738
  | `@utilix-tech/sdk/api` | cURL build/parse, JWT decode/sign, JWKS parse, CORS headers, CSP header |
714
739
  | `@utilix-tech/sdk/code` | SQL format/minify, HTML format/minify, regex tester, GraphQL format, MIME type lookup, GitHub URL parsing, regex ReDoS detector, Conventional Commits linter |
@@ -14,12 +14,15 @@ __export(units_exports, {
14
14
  COMMON_TAX_RATES: () => COMMON_TAX_RATES,
15
15
  COMMON_TIP_PERCENTAGES: () => COMMON_TIP_PERCENTAGES,
16
16
  COMMON_VIEWPORTS: () => COMMON_VIEWPORTS,
17
+ USD_DENOMINATIONS: () => USD_DENOMINATIONS,
17
18
  applySuccessiveDiscounts: () => applySuccessiveDiscounts,
18
19
  autoFormat: () => autoFormat,
19
20
  bumpVersion: () => bumpVersion,
20
21
  calcAspectRatio: () => calcAspectRatio,
21
22
  calcSubnet: () => calcSubnet,
22
23
  calculateBreakEven: () => calculateBreakEven,
24
+ calculateCashBreakdown: () => calculateCashBreakdown,
25
+ calculateChange: () => calculateChange,
23
26
  calculateCompoundInterest: () => calculateCompoundInterest,
24
27
  calculateDiscount: () => calculateDiscount,
25
28
  calculateEmi: () => calculateEmi,
@@ -1867,5 +1870,63 @@ function compareUnitPrices(items) {
1867
1870
  const bestLabel = results.find((r) => r.isBest).label;
1868
1871
  return { items: results, bestLabel };
1869
1872
  }
1873
+ var USD_DENOMINATIONS = [100, 50, 20, 10, 5, 1, 0.25, 0.1, 0.05, 0.01];
1874
+ function _cashToCents(n) {
1875
+ return Math.round(n * 100);
1876
+ }
1877
+ function calculateCashBreakdown(amount, denominations) {
1878
+ if (!Number.isFinite(amount) || amount < 0) return { error: "Amount must be zero or a positive number" };
1879
+ if (!Array.isArray(denominations) || denominations.length === 0) {
1880
+ return { error: "At least one denomination is required" };
1881
+ }
1882
+ for (const d of denominations) {
1883
+ if (typeof d !== "number" || !Number.isFinite(d) || d <= 0) {
1884
+ return { error: "Each denomination must be a positive number" };
1885
+ }
1886
+ }
1887
+ const round22 = (n) => Math.round(n * 100) / 100;
1888
+ const sorted = Array.from(new Set(denominations)).sort((a, b) => b - a);
1889
+ let remainingCents = _cashToCents(amount);
1890
+ const breakdown = [];
1891
+ let totalCount = 0;
1892
+ for (const d of sorted) {
1893
+ const dCents = _cashToCents(d);
1894
+ const count = Math.floor(remainingCents / dCents);
1895
+ if (count > 0) {
1896
+ breakdown.push({ denomination: d, count, subtotal: round22(count * d) });
1897
+ remainingCents -= count * dCents;
1898
+ totalCount += count;
1899
+ }
1900
+ }
1901
+ return {
1902
+ amount: round22(amount),
1903
+ denominations: sorted,
1904
+ breakdown,
1905
+ totalCount,
1906
+ remainder: round22(remainingCents / 100)
1907
+ };
1908
+ }
1909
+ function calculateChange(amountDue, amountTendered, denominations) {
1910
+ if (!Number.isFinite(amountDue) || amountDue < 0) return { error: "Amount due must be zero or a positive number" };
1911
+ if (!Number.isFinite(amountTendered) || amountTendered < 0) {
1912
+ return { error: "Amount tendered must be zero or a positive number" };
1913
+ }
1914
+ if (_cashToCents(amountTendered) < _cashToCents(amountDue)) {
1915
+ return { error: "Amount tendered must be greater than or equal to the amount due" };
1916
+ }
1917
+ const round22 = (n) => Math.round(n * 100) / 100;
1918
+ const changeDue = round22(amountTendered - amountDue);
1919
+ const breakdown = calculateCashBreakdown(changeDue, denominations);
1920
+ if ("error" in breakdown) return breakdown;
1921
+ return {
1922
+ amountDue: round22(amountDue),
1923
+ amountTendered: round22(amountTendered),
1924
+ changeDue,
1925
+ denominations: breakdown.denominations,
1926
+ breakdown: breakdown.breakdown,
1927
+ totalCount: breakdown.totalCount,
1928
+ remainder: breakdown.remainder
1929
+ };
1930
+ }
1870
1931
 
1871
- export { BASE_LABELS, BASE_PREFIXES, BYTE_UNITS, COMMON_CURRENCIES, COMMON_LOCALES, COMMON_PERMISSIONS, COMMON_RATIOS, COMMON_SIZES, COMMON_TAX_RATES, COMMON_TIP_PERCENTAGES, COMMON_VIEWPORTS, applySuccessiveDiscounts, autoFormat, bumpVersion, calcAspectRatio, calcSubnet, calculateBreakEven, calculateCompoundInterest, calculateDiscount, calculateEmi, calculateLoan, calculateMarginMarkup, calculatePercentageChange, calculateRoi, calculateRuleOf72, calculateSavingsGoal, calculateSimpleInterest, calculateTax, calculateTimeValue, calculateTipSplit, calculateVideoBitrate, compareSemver, compareUnitPrices, convert, convertAprApy, convertBytes, convertIp, coverBox, decimalToIp, decimalToIpCidr, describePermissions, fitIntoBox, formatNumber, formatValue, formatVwValue, formatWithSeparator, gcd, ipFromBinary, ipFromDecimal, ipFromHex, ipToBinary, ipToDecimal, ipToDecimalCidr, ipToHex, ipToOctal, isValid, isValidCidr, isValidIp, isValidIpv4, marginToMarkup, markupToMargin, octalToPermissions, parseFileSize, parseInput, parseNumberString, parseSemver, permissionsToChmod, permissionsToOctal, permissionsToSymbolic, ptToPx, pxToAll, pxToViewport, remToPx, satisfiesRange, scaleByHeight, scaleByWidth, sortVersions, splitSubnet, symbolicToPermissions, units_exports, validateBarcode, validateCardNumber, validateIban, validateIsbn, viewportToPx };
1932
+ export { BASE_LABELS, BASE_PREFIXES, BYTE_UNITS, COMMON_CURRENCIES, COMMON_LOCALES, COMMON_PERMISSIONS, COMMON_RATIOS, COMMON_SIZES, COMMON_TAX_RATES, COMMON_TIP_PERCENTAGES, COMMON_VIEWPORTS, USD_DENOMINATIONS, applySuccessiveDiscounts, autoFormat, bumpVersion, calcAspectRatio, calcSubnet, calculateBreakEven, calculateCashBreakdown, calculateChange, calculateCompoundInterest, calculateDiscount, calculateEmi, calculateLoan, calculateMarginMarkup, calculatePercentageChange, calculateRoi, calculateRuleOf72, calculateSavingsGoal, calculateSimpleInterest, calculateTax, calculateTimeValue, calculateTipSplit, calculateVideoBitrate, compareSemver, compareUnitPrices, convert, convertAprApy, convertBytes, convertIp, coverBox, decimalToIp, decimalToIpCidr, describePermissions, fitIntoBox, formatNumber, formatValue, formatVwValue, formatWithSeparator, gcd, ipFromBinary, ipFromDecimal, ipFromHex, ipToBinary, ipToDecimal, ipToDecimalCidr, ipToHex, ipToOctal, isValid, isValidCidr, isValidIp, isValidIpv4, marginToMarkup, markupToMargin, octalToPermissions, parseFileSize, parseInput, parseNumberString, parseSemver, permissionsToChmod, permissionsToOctal, permissionsToSymbolic, ptToPx, pxToAll, pxToViewport, remToPx, satisfiesRange, scaleByHeight, scaleByWidth, sortVersions, splitSubnet, symbolicToPermissions, units_exports, validateBarcode, validateCardNumber, validateIban, validateIsbn, viewportToPx };
@@ -14,6 +14,7 @@ __export(network_exports, {
14
14
  buildGeoUrl: () => buildGeoUrl,
15
15
  buildLinkHeader: () => buildLinkHeader,
16
16
  buildMapUrl: () => buildMapUrl,
17
+ buildRangeHeader: () => buildRangeHeader,
17
18
  buildReferrerPolicy: () => buildReferrerPolicy,
18
19
  checkSetCookieHeader: () => checkSetCookieHeader,
19
20
  countryCodeToFlag: () => countryCodeToFlag,
@@ -30,6 +31,7 @@ __export(network_exports, {
30
31
  isValidIp: () => isValidIp,
31
32
  isValidIpv4: () => isValidIpv4,
32
33
  isValidIpv6: () => isValidIpv6,
34
+ parseContentRangeHeader: () => parseContentRangeHeader,
33
35
  parseDnsResponse: () => parseDnsResponse,
34
36
  parseGeoResponse: () => parseGeoResponse,
35
37
  parseHar: () => parseHar,
@@ -2209,5 +2211,118 @@ function analyzeEmailHeaders(raw) {
2209
2211
  warnings
2210
2212
  };
2211
2213
  }
2214
+ function _round2(n) {
2215
+ return Math.round(n * 100) / 100;
2216
+ }
2217
+ function buildRangeHeader(entries, options) {
2218
+ if (!Array.isArray(entries) || entries.length === 0) {
2219
+ return { error: "At least one range is required" };
2220
+ }
2221
+ const unit = options?.unit?.trim() || "bytes";
2222
+ const totalSize = typeof options?.totalSize === "number" && Number.isFinite(options.totalSize) ? options.totalSize : void 0;
2223
+ if (totalSize !== void 0 && totalSize <= 0) {
2224
+ return { error: "totalSize must be a positive number when provided" };
2225
+ }
2226
+ const parsed = [];
2227
+ for (const entry of entries) {
2228
+ if (entry.suffixLength !== void 0) {
2229
+ if (!Number.isFinite(entry.suffixLength) || entry.suffixLength <= 0) {
2230
+ return { error: "suffixLength must be a positive number" };
2231
+ }
2232
+ const suffixLength = Math.floor(entry.suffixLength);
2233
+ const byteLength = totalSize !== void 0 ? Math.min(suffixLength, totalSize) : suffixLength;
2234
+ parsed.push({
2235
+ spec: `-${suffixLength}`,
2236
+ start: null,
2237
+ end: null,
2238
+ suffixLength,
2239
+ byteLength,
2240
+ percentOfTotal: totalSize !== void 0 ? _round2(byteLength / totalSize * 100) : null
2241
+ });
2242
+ continue;
2243
+ }
2244
+ if (entry.start === void 0) {
2245
+ return { error: "Each range requires either a start byte or a suffixLength" };
2246
+ }
2247
+ if (!Number.isFinite(entry.start) || entry.start < 0) {
2248
+ return { error: "start must be a non-negative number" };
2249
+ }
2250
+ const start = Math.floor(entry.start);
2251
+ if (totalSize !== void 0 && start >= totalSize) {
2252
+ return { error: `start byte ${start} is beyond totalSize (${totalSize})` };
2253
+ }
2254
+ if (entry.end !== void 0) {
2255
+ if (!Number.isFinite(entry.end) || entry.end < 0) {
2256
+ return { error: "end must be a non-negative number" };
2257
+ }
2258
+ const end = Math.floor(entry.end);
2259
+ if (end < start) {
2260
+ return { error: `range end (${end}) must be >= start (${start})` };
2261
+ }
2262
+ if (totalSize !== void 0 && end >= totalSize) {
2263
+ return { error: `end byte ${end} is beyond totalSize (${totalSize})` };
2264
+ }
2265
+ const byteLength = end - start + 1;
2266
+ parsed.push({
2267
+ spec: `${start}-${end}`,
2268
+ start,
2269
+ end,
2270
+ suffixLength: null,
2271
+ byteLength,
2272
+ percentOfTotal: totalSize !== void 0 ? _round2(byteLength / totalSize * 100) : null
2273
+ });
2274
+ } else {
2275
+ const byteLength = totalSize !== void 0 ? totalSize - start : null;
2276
+ parsed.push({
2277
+ spec: `${start}-`,
2278
+ start,
2279
+ end: null,
2280
+ suffixLength: null,
2281
+ byteLength,
2282
+ percentOfTotal: byteLength !== null && totalSize !== void 0 ? _round2(byteLength / totalSize * 100) : null
2283
+ });
2284
+ }
2285
+ }
2286
+ const warnings = [];
2287
+ const resolvable = parsed.map((r, i) => ({ i, start: r.start ?? (totalSize !== void 0 && r.suffixLength !== null ? totalSize - Math.min(r.suffixLength, totalSize) : null), end: r.end ?? (totalSize !== void 0 ? totalSize - 1 : null) })).filter((r) => r.start !== null && r.end !== null).sort((a, b) => a.start - b.start);
2288
+ for (let i = 1; i < resolvable.length; i++) {
2289
+ if (resolvable[i].start <= resolvable[i - 1].end) {
2290
+ warnings.push(`ranges "${parsed[resolvable[i - 1].i].spec}" and "${parsed[resolvable[i].i].spec}" overlap - servers may reject overlapping multi-range requests`);
2291
+ }
2292
+ }
2293
+ if (parsed.length > 1 && parsed.some((r) => r.byteLength === null)) {
2294
+ warnings.push("totalSize was not provided, so open-ended ranges cannot be checked for overlap or total byte count");
2295
+ }
2296
+ const totalBytesRequested = parsed.every((r) => r.byteLength !== null) ? parsed.reduce((sum, r) => sum + (r.byteLength ?? 0), 0) : null;
2297
+ const headerValue = `${unit}=${parsed.map((r) => r.spec).join(",")}`;
2298
+ return {
2299
+ header: `Range: ${headerValue}`,
2300
+ headerValue,
2301
+ ranges: parsed,
2302
+ totalBytesRequested,
2303
+ warnings
2304
+ };
2305
+ }
2306
+ function parseContentRangeHeader(header) {
2307
+ if (!header || header.trim() === "") return { error: "Input is empty" };
2308
+ const match = header.trim().match(/^(\S+)\s+(\*|\d+-\d+)\/(\*|\d+)$/);
2309
+ if (!match) {
2310
+ return { error: 'Could not parse Content-Range header. Expected a format like "bytes 200-1000/67589" or "bytes */67589"' };
2311
+ }
2312
+ const [, unit, range, totalRaw] = match;
2313
+ const isUnsatisfiable = range === "*";
2314
+ const isTotalUnknown = totalRaw === "*";
2315
+ const total = isTotalUnknown ? null : Number(totalRaw);
2316
+ if (isUnsatisfiable) {
2317
+ return { unit, start: null, end: null, total, isUnsatisfiable, isTotalUnknown, byteLength: null };
2318
+ }
2319
+ const [startRaw, endRaw] = range.split("-");
2320
+ const start = Number(startRaw);
2321
+ const end = Number(endRaw);
2322
+ if (end < start) {
2323
+ return { error: `Invalid range: end (${end}) is before start (${start})` };
2324
+ }
2325
+ return { unit, start, end, total, isUnsatisfiable, isTotalUnknown, byteLength: end - start + 1 };
2326
+ }
2212
2327
 
2213
- export { DNS_RECORD_TYPES, HTTP_HEADERS, LINK_REL_VALUES, REFERRER_POLICY_VALUES, STATUS_CODES, analyzeEmailHeaders, buildCacheControl, buildDnsQueryUrl, buildGeoUrl, buildLinkHeader, buildMapUrl, buildReferrerPolicy, checkSetCookieHeader, countryCodeToFlag, formatCoordinates, formatTtl, getByCategory, getHeader, getHeadersByCategory, getStatusByCategory, getStatusCode, isClientError, isServerError, isSuccess, isValidIp, isValidIpv4, isValidIpv6, network_exports, parseDnsResponse, parseGeoResponse, parseHar, parseIpv6Address, parseUserAgent, searchHeaders, searchStatusCodes, statusCodeToText, typeNumberToLabel, validateDomain, validateHeaderName, validateHeaderValue, validateRobotsTxt, validateSitemap };
2328
+ export { DNS_RECORD_TYPES, HTTP_HEADERS, LINK_REL_VALUES, REFERRER_POLICY_VALUES, STATUS_CODES, analyzeEmailHeaders, buildCacheControl, buildDnsQueryUrl, buildGeoUrl, buildLinkHeader, buildMapUrl, buildRangeHeader, buildReferrerPolicy, checkSetCookieHeader, countryCodeToFlag, formatCoordinates, formatTtl, getByCategory, getHeader, getHeadersByCategory, getStatusByCategory, getStatusCode, isClientError, isServerError, isSuccess, isValidIp, isValidIpv4, isValidIpv6, network_exports, parseContentRangeHeader, parseDnsResponse, parseGeoResponse, parseHar, parseIpv6Address, parseUserAgent, searchHeaders, searchStatusCodes, statusCodeToText, typeNumberToLabel, validateDomain, validateHeaderName, validateHeaderValue, validateRobotsTxt, validateSitemap };
@@ -13,6 +13,7 @@ __export(data_exports, {
13
13
  diffEnvExample: () => diffEnvExample,
14
14
  envToExport: () => envToExport,
15
15
  envToJson: () => envToJson,
16
+ findCsvDuplicateRows: () => findCsvDuplicateRows,
16
17
  formatNdjson: () => formatNdjson,
17
18
  formatSubtitleTimestamp: () => formatSubtitleTimestamp,
18
19
  formatToml: () => formatToml,
@@ -1500,5 +1501,64 @@ function csvToSqlInsert(input, options) {
1500
1501
  columns
1501
1502
  };
1502
1503
  }
1504
+ function _normalizeDuplicateValue(value, caseSensitive, trimWhitespace) {
1505
+ let v = value;
1506
+ if (trimWhitespace) v = v.trim();
1507
+ if (!caseSensitive) v = v.toLowerCase();
1508
+ return v;
1509
+ }
1510
+ function findCsvDuplicateRows(input, options) {
1511
+ const parsed = parseCsv(input, {
1512
+ delimiter: options?.delimiter ?? ",",
1513
+ hasHeader: options?.hasHeader !== false,
1514
+ trimWhitespace: false
1515
+ });
1516
+ if ("error" in parsed) return parsed;
1517
+ if (parsed.rows.length === 0) return { error: "No data rows found" };
1518
+ const caseSensitive = options?.caseSensitive !== false;
1519
+ const trimWhitespace = options?.trimWhitespace !== false;
1520
+ let keyIndexes;
1521
+ if (options?.keyColumns && options.keyColumns.length > 0) {
1522
+ keyIndexes = [];
1523
+ for (const col of options.keyColumns) {
1524
+ const idx = parsed.headers.indexOf(col);
1525
+ if (idx === -1) {
1526
+ return { error: `Column "${col}" not found. Available columns: ${parsed.headers.join(", ")}` };
1527
+ }
1528
+ keyIndexes.push(idx);
1529
+ }
1530
+ } else {
1531
+ keyIndexes = parsed.headers.map((_, idx) => idx);
1532
+ }
1533
+ const groups = /* @__PURE__ */ new Map();
1534
+ parsed.rows.forEach((row, i) => {
1535
+ const key = keyIndexes.map((idx) => row[idx] ?? "");
1536
+ const normalizedKey = keyIndexes.map((idx) => _normalizeDuplicateValue(row[idx] ?? "", caseSensitive, trimWhitespace));
1537
+ const mapKey = JSON.stringify(normalizedKey);
1538
+ const rowNumber = i + 1;
1539
+ const existing = groups.get(mapKey);
1540
+ if (existing) {
1541
+ existing.rowNumbers.push(rowNumber);
1542
+ } else {
1543
+ groups.set(mapKey, { key, rowNumbers: [rowNumber] });
1544
+ }
1545
+ });
1546
+ const duplicateGroups = [];
1547
+ let duplicateRowCount = 0;
1548
+ for (const group of groups.values()) {
1549
+ if (group.rowNumbers.length > 1) {
1550
+ duplicateGroups.push({ key: group.key, rowNumbers: group.rowNumbers, count: group.rowNumbers.length });
1551
+ duplicateRowCount += group.rowNumbers.length;
1552
+ }
1553
+ }
1554
+ duplicateGroups.sort((a, b) => b.count - a.count);
1555
+ return {
1556
+ headers: parsed.headers,
1557
+ totalRows: parsed.rows.length,
1558
+ duplicateGroups,
1559
+ duplicateRowCount,
1560
+ uniqueRowCount: parsed.rows.length - duplicateRowCount + duplicateGroups.length
1561
+ };
1562
+ }
1503
1563
 
1504
- export { convertSubtitles, csvToJson, csvToSqlInsert, data_exports, detectDelimiter, detectSubtitleFormat, diffEnvExample, envToExport, envToJson, formatNdjson, formatSubtitleTimestamp, formatToml, formatXml, generateMarkdownToc, getSection, getSectionNames, getTomlStats, getXmlStats, getYamlStats, iniToJson, jsonArrayToNdjson, jsonToIni, jsonToToml, jsonToXml, minifyNdjson, minifyToml, minifyXml, ndjsonToJsonArray, parseCsv, parseEnv, parseFrontMatter, parseIni, parseSubtitleTimestamp, parseSubtitles, parseXmlToObject, serializeSubtitles, shiftSubtitles, stringifyIni, toCsvString, tomlToJson, validateEnvKey, validateIni, validateNdjson, validateToml, validateXml, validateYaml, xmlToJson };
1564
+ export { convertSubtitles, csvToJson, csvToSqlInsert, data_exports, detectDelimiter, detectSubtitleFormat, diffEnvExample, envToExport, envToJson, findCsvDuplicateRows, formatNdjson, formatSubtitleTimestamp, formatToml, formatXml, generateMarkdownToc, getSection, getSectionNames, getTomlStats, getXmlStats, getYamlStats, iniToJson, jsonArrayToNdjson, jsonToIni, jsonToToml, jsonToXml, minifyNdjson, minifyToml, minifyXml, ndjsonToJsonArray, parseCsv, parseEnv, parseFrontMatter, parseIni, parseSubtitleTimestamp, parseSubtitles, parseXmlToObject, serializeSubtitles, shiftSubtitles, stringifyIni, toCsvString, tomlToJson, validateEnvKey, validateIni, validateNdjson, validateToml, validateXml, validateYaml, xmlToJson };
@@ -6,12 +6,14 @@ import bcrypt from 'bcryptjs';
6
6
  // src/tools/hashing.ts
7
7
  var hashing_exports = {};
8
8
  __export(hashing_exports, {
9
+ ETAG_ALGORITHMS: () => ETAG_ALGORITHMS,
9
10
  HASH_ALGORITHMS: () => HASH_ALGORITHMS,
10
11
  HASH_BITS: () => HASH_BITS,
11
12
  ROUND_OPTIONS: () => ROUND_OPTIONS,
12
13
  SRI_ALGORITHMS: () => SRI_ALGORITHMS,
13
14
  createEntry: () => createEntry,
14
15
  generateBcryptHash: () => generateBcryptHash,
16
+ generateEtag: () => generateEtag,
15
17
  generateHtpasswdFile: () => generateHtpasswdFile,
16
18
  generateMd5Hash: () => generateMd5Hash,
17
19
  generatePlain: () => generatePlain,
@@ -272,5 +274,23 @@ function generateSriAll(content) {
272
274
  }
273
275
  return SRI_ALGORITHMS.map((alg) => generateSri(content, alg));
274
276
  }
277
+ var ETAG_ALGORITHMS = ["MD5", "SHA-1", "SHA-256"];
278
+ var ETAG_NODE_ALG_MAP = {
279
+ "MD5": "md5",
280
+ "SHA-1": "sha1",
281
+ "SHA-256": "sha256"
282
+ };
283
+ function generateEtag(content, algorithm = "SHA-1", weak = false) {
284
+ if (typeof content !== "string" || content.length === 0) {
285
+ return { error: "Content is required" };
286
+ }
287
+ if (!ETAG_ALGORITHMS.includes(algorithm)) {
288
+ return { error: `Unknown algorithm: ${algorithm}. Must be one of ${ETAG_ALGORITHMS.join(", ")}` };
289
+ }
290
+ const hash = createHash(ETAG_NODE_ALG_MAP[algorithm]).update(content, "utf8").digest("hex");
291
+ const quoted = `"${hash}"`;
292
+ const etag = weak ? `W/${quoted}` : quoted;
293
+ return { algorithm, weak, hash, etag, headerName: "ETag", snippet: `ETag: ${etag}` };
294
+ }
275
295
 
276
- export { HASH_ALGORITHMS, HASH_BITS, ROUND_OPTIONS, SRI_ALGORITHMS, createEntry, generateBcryptHash, generateHtpasswdFile, generateMd5Hash, generatePlain, generateSha1Hash, generateSri, generateSriAll, hashAll, hashOne, hashPassword, hashWithTiming, hashing_exports, parseHtpasswdFile, validateRounds, validateUsername, verifyPassword };
296
+ export { ETAG_ALGORITHMS, HASH_ALGORITHMS, HASH_BITS, ROUND_OPTIONS, SRI_ALGORITHMS, createEntry, generateBcryptHash, generateEtag, generateHtpasswdFile, generateMd5Hash, generatePlain, generateSha1Hash, generateSri, generateSriAll, hashAll, hashOne, hashPassword, hashWithTiming, hashing_exports, parseHtpasswdFile, validateRounds, validateUsername, verifyPassword };
@@ -16,6 +16,7 @@
16
16
  * - markdown-front-matter-parser: parseFrontMatter
17
17
  * - markdown-toc-generator: generateMarkdownToc
18
18
  * - csv-to-sql-insert-generator: csvToSqlInsert
19
+ * - csv-duplicate-row-finder: findCsvDuplicateRows
19
20
  */
20
21
  interface ValidationResult {
21
22
  valid: boolean;
@@ -343,8 +344,39 @@ interface CsvToSqlResult {
343
344
  declare function csvToSqlInsert(input: string, options?: CsvToSqlOptions): CsvToSqlResult | {
344
345
  error: string;
345
346
  };
347
+ interface CsvDuplicateOptions {
348
+ delimiter?: string;
349
+ hasHeader?: boolean;
350
+ keyColumns?: string[];
351
+ caseSensitive?: boolean;
352
+ trimWhitespace?: boolean;
353
+ }
354
+ interface CsvDuplicateGroup {
355
+ key: string[];
356
+ rowNumbers: number[];
357
+ count: number;
358
+ }
359
+ interface CsvDuplicateResult {
360
+ headers: string[];
361
+ totalRows: number;
362
+ duplicateGroups: CsvDuplicateGroup[];
363
+ duplicateRowCount: number;
364
+ uniqueRowCount: number;
365
+ }
366
+ /**
367
+ * Parse CSV/TSV data and group rows that share the same value across a
368
+ * chosen set of key columns (or the entire row, if no key columns are
369
+ * given), so you can spot and clean up accidental duplicates before an
370
+ * import.
371
+ */
372
+ declare function findCsvDuplicateRows(input: string, options?: CsvDuplicateOptions): CsvDuplicateResult | {
373
+ error: string;
374
+ };
346
375
 
347
376
  type data_ConvertedSubtitles = ConvertedSubtitles;
377
+ type data_CsvDuplicateGroup = CsvDuplicateGroup;
378
+ type data_CsvDuplicateOptions = CsvDuplicateOptions;
379
+ type data_CsvDuplicateResult = CsvDuplicateResult;
348
380
  type data_CsvParseResult = CsvParseResult;
349
381
  type data_CsvToSqlOptions = CsvToSqlOptions;
350
382
  type data_CsvToSqlResult = CsvToSqlResult;
@@ -377,6 +409,7 @@ declare const data_detectSubtitleFormat: typeof detectSubtitleFormat;
377
409
  declare const data_diffEnvExample: typeof diffEnvExample;
378
410
  declare const data_envToExport: typeof envToExport;
379
411
  declare const data_envToJson: typeof envToJson;
412
+ declare const data_findCsvDuplicateRows: typeof findCsvDuplicateRows;
380
413
  declare const data_formatNdjson: typeof formatNdjson;
381
414
  declare const data_formatSubtitleTimestamp: typeof formatSubtitleTimestamp;
382
415
  declare const data_formatToml: typeof formatToml;
@@ -416,7 +449,7 @@ declare const data_validateXml: typeof validateXml;
416
449
  declare const data_validateYaml: typeof validateYaml;
417
450
  declare const data_xmlToJson: typeof xmlToJson;
418
451
  declare namespace data {
419
- export { type data_ConvertedSubtitles as ConvertedSubtitles, type data_CsvParseResult as CsvParseResult, type data_CsvToSqlOptions as CsvToSqlOptions, type data_CsvToSqlResult as CsvToSqlResult, type data_EnvDiffError as EnvDiffError, type data_EnvDiffResult as EnvDiffResult, type data_EnvEntry as EnvEntry, type data_EnvParseResult as EnvParseResult, type data_FrontMatterError as FrontMatterError, type data_FrontMatterResult as FrontMatterResult, type data_IniSection as IniSection, type data_MarkdownTocEntry as MarkdownTocEntry, type data_MarkdownTocOptions as MarkdownTocOptions, type data_MarkdownTocResult as MarkdownTocResult, type data_NdjsonLineResult as NdjsonLineResult, type data_NdjsonValidateResult as NdjsonValidateResult, type data_ParseOptions as ParseOptions, type data_ParsedIni as ParsedIni, type data_ParsedSubtitles as ParsedSubtitles, type data_SubtitleCue as SubtitleCue, type data_SubtitleFormat as SubtitleFormat, type data_TomlFormatResult as TomlFormatResult, type data_ValidationResult as ValidationResult, type data_XmlFormatResult as XmlFormatResult, type data_XmlToJsonOptions as XmlToJsonOptions, data_convertSubtitles as convertSubtitles, data_csvToJson as csvToJson, data_csvToSqlInsert as csvToSqlInsert, data_detectDelimiter as detectDelimiter, data_detectSubtitleFormat as detectSubtitleFormat, data_diffEnvExample as diffEnvExample, data_envToExport as envToExport, data_envToJson as envToJson, data_formatNdjson as formatNdjson, data_formatSubtitleTimestamp as formatSubtitleTimestamp, data_formatToml as formatToml, data_formatXml as formatXml, data_generateMarkdownToc as generateMarkdownToc, data_getSection as getSection, data_getSectionNames as getSectionNames, data_getTomlStats as getTomlStats, data_getXmlStats as getXmlStats, data_getYamlStats as getYamlStats, data_iniToJson as iniToJson, data_jsonArrayToNdjson as jsonArrayToNdjson, data_jsonToIni as jsonToIni, data_jsonToToml as jsonToToml, data_jsonToXml as jsonToXml, data_minifyNdjson as minifyNdjson, data_minifyToml as minifyToml, data_minifyXml as minifyXml, data_ndjsonToJsonArray as ndjsonToJsonArray, data_parseCsv as parseCsv, data_parseEnv as parseEnv, data_parseFrontMatter as parseFrontMatter, data_parseIni as parseIni, data_parseSubtitleTimestamp as parseSubtitleTimestamp, data_parseSubtitles as parseSubtitles, data_parseXmlToObject as parseXmlToObject, data_serializeSubtitles as serializeSubtitles, data_shiftSubtitles as shiftSubtitles, data_stringifyIni as stringifyIni, data_toCsvString as toCsvString, data_tomlToJson as tomlToJson, data_validateEnvKey as validateEnvKey, data_validateIni as validateIni, data_validateNdjson as validateNdjson, data_validateToml as validateToml, data_validateXml as validateXml, data_validateYaml as validateYaml, data_xmlToJson as xmlToJson };
452
+ export { type data_ConvertedSubtitles as ConvertedSubtitles, type data_CsvDuplicateGroup as CsvDuplicateGroup, type data_CsvDuplicateOptions as CsvDuplicateOptions, type data_CsvDuplicateResult as CsvDuplicateResult, type data_CsvParseResult as CsvParseResult, type data_CsvToSqlOptions as CsvToSqlOptions, type data_CsvToSqlResult as CsvToSqlResult, type data_EnvDiffError as EnvDiffError, type data_EnvDiffResult as EnvDiffResult, type data_EnvEntry as EnvEntry, type data_EnvParseResult as EnvParseResult, type data_FrontMatterError as FrontMatterError, type data_FrontMatterResult as FrontMatterResult, type data_IniSection as IniSection, type data_MarkdownTocEntry as MarkdownTocEntry, type data_MarkdownTocOptions as MarkdownTocOptions, type data_MarkdownTocResult as MarkdownTocResult, type data_NdjsonLineResult as NdjsonLineResult, type data_NdjsonValidateResult as NdjsonValidateResult, type data_ParseOptions as ParseOptions, type data_ParsedIni as ParsedIni, type data_ParsedSubtitles as ParsedSubtitles, type data_SubtitleCue as SubtitleCue, type data_SubtitleFormat as SubtitleFormat, type data_TomlFormatResult as TomlFormatResult, type data_ValidationResult as ValidationResult, type data_XmlFormatResult as XmlFormatResult, type data_XmlToJsonOptions as XmlToJsonOptions, data_convertSubtitles as convertSubtitles, data_csvToJson as csvToJson, data_csvToSqlInsert as csvToSqlInsert, data_detectDelimiter as detectDelimiter, data_detectSubtitleFormat as detectSubtitleFormat, data_diffEnvExample as diffEnvExample, data_envToExport as envToExport, data_envToJson as envToJson, data_findCsvDuplicateRows as findCsvDuplicateRows, data_formatNdjson as formatNdjson, data_formatSubtitleTimestamp as formatSubtitleTimestamp, data_formatToml as formatToml, data_formatXml as formatXml, data_generateMarkdownToc as generateMarkdownToc, data_getSection as getSection, data_getSectionNames as getSectionNames, data_getTomlStats as getTomlStats, data_getXmlStats as getXmlStats, data_getYamlStats as getYamlStats, data_iniToJson as iniToJson, data_jsonArrayToNdjson as jsonArrayToNdjson, data_jsonToIni as jsonToIni, data_jsonToToml as jsonToToml, data_jsonToXml as jsonToXml, data_minifyNdjson as minifyNdjson, data_minifyToml as minifyToml, data_minifyXml as minifyXml, data_ndjsonToJsonArray as ndjsonToJsonArray, data_parseCsv as parseCsv, data_parseEnv as parseEnv, data_parseFrontMatter as parseFrontMatter, data_parseIni as parseIni, data_parseSubtitleTimestamp as parseSubtitleTimestamp, data_parseSubtitles as parseSubtitles, data_parseXmlToObject as parseXmlToObject, data_serializeSubtitles as serializeSubtitles, data_shiftSubtitles as shiftSubtitles, data_stringifyIni as stringifyIni, data_toCsvString as toCsvString, data_tomlToJson as tomlToJson, data_validateEnvKey as validateEnvKey, data_validateIni as validateIni, data_validateNdjson as validateNdjson, data_validateToml as validateToml, data_validateXml as validateXml, data_validateYaml as validateYaml, data_xmlToJson as xmlToJson };
420
453
  }
421
454
 
422
- export { parseCsv as $, formatXml as A, generateMarkdownToc as B, type ConvertedSubtitles as C, getSection as D, type EnvDiffError as E, type FrontMatterError as F, getSectionNames as G, getTomlStats as H, type IniSection as I, getXmlStats as J, getYamlStats as K, iniToJson as L, type MarkdownTocEntry as M, type NdjsonLineResult as N, jsonArrayToNdjson as O, type ParseOptions as P, jsonToIni as Q, jsonToToml as R, type SubtitleCue as S, type TomlFormatResult as T, jsonToXml as U, type ValidationResult as V, minifyNdjson as W, type XmlFormatResult as X, minifyToml as Y, minifyXml as Z, ndjsonToJsonArray as _, type CsvParseResult as a, parseEnv as a0, parseFrontMatter as a1, parseIni as a2, parseSubtitleTimestamp as a3, parseSubtitles as a4, parseXmlToObject as a5, serializeSubtitles as a6, shiftSubtitles as a7, stringifyIni as a8, toCsvString as a9, tomlToJson as aa, validateEnvKey as ab, validateIni as ac, validateNdjson as ad, validateToml as ae, validateXml as af, validateYaml as ag, xmlToJson as ah, type CsvToSqlOptions as b, type CsvToSqlResult as c, data as d, type EnvDiffResult as e, type EnvEntry as f, type EnvParseResult as g, type FrontMatterResult as h, type MarkdownTocOptions as i, type MarkdownTocResult as j, type NdjsonValidateResult as k, type ParsedIni as l, type ParsedSubtitles as m, type SubtitleFormat as n, type XmlToJsonOptions as o, convertSubtitles as p, csvToJson as q, csvToSqlInsert as r, detectDelimiter as s, detectSubtitleFormat as t, diffEnvExample as u, envToExport as v, envToJson as w, formatNdjson as x, formatSubtitleTimestamp as y, formatToml as z };
455
+ export { minifyNdjson as $, findCsvDuplicateRows as A, formatNdjson as B, type ConvertedSubtitles as C, formatSubtitleTimestamp as D, type EnvDiffError as E, type FrontMatterError as F, formatToml as G, formatXml as H, type IniSection as I, generateMarkdownToc as J, getSection as K, getSectionNames as L, type MarkdownTocEntry as M, type NdjsonLineResult as N, getTomlStats as O, type ParseOptions as P, getXmlStats as Q, getYamlStats as R, type SubtitleCue as S, type TomlFormatResult as T, iniToJson as U, type ValidationResult as V, jsonArrayToNdjson as W, type XmlFormatResult as X, jsonToIni as Y, jsonToToml as Z, jsonToXml as _, type CsvDuplicateGroup as a, minifyToml as a0, minifyXml as a1, ndjsonToJsonArray as a2, parseCsv as a3, parseEnv as a4, parseFrontMatter as a5, parseIni as a6, parseSubtitleTimestamp as a7, parseSubtitles as a8, parseXmlToObject as a9, serializeSubtitles as aa, shiftSubtitles as ab, stringifyIni as ac, toCsvString as ad, tomlToJson as ae, validateEnvKey as af, validateIni as ag, validateNdjson as ah, validateToml as ai, validateXml as aj, validateYaml as ak, xmlToJson as al, type CsvDuplicateOptions as b, type CsvDuplicateResult as c, data as d, type CsvParseResult as e, type CsvToSqlOptions as f, type CsvToSqlResult as g, type EnvDiffResult as h, type EnvEntry as i, type EnvParseResult as j, type FrontMatterResult as k, type MarkdownTocOptions as l, type MarkdownTocResult as m, type NdjsonValidateResult as n, type ParsedIni as o, type ParsedSubtitles as p, type SubtitleFormat as q, type XmlToJsonOptions as r, convertSubtitles as s, csvToJson as t, csvToSqlInsert as u, detectDelimiter as v, detectSubtitleFormat as w, diffEnvExample as x, envToExport as y, envToJson as z };
@@ -16,6 +16,7 @@
16
16
  * - markdown-front-matter-parser: parseFrontMatter
17
17
  * - markdown-toc-generator: generateMarkdownToc
18
18
  * - csv-to-sql-insert-generator: csvToSqlInsert
19
+ * - csv-duplicate-row-finder: findCsvDuplicateRows
19
20
  */
20
21
  interface ValidationResult {
21
22
  valid: boolean;
@@ -343,8 +344,39 @@ interface CsvToSqlResult {
343
344
  declare function csvToSqlInsert(input: string, options?: CsvToSqlOptions): CsvToSqlResult | {
344
345
  error: string;
345
346
  };
347
+ interface CsvDuplicateOptions {
348
+ delimiter?: string;
349
+ hasHeader?: boolean;
350
+ keyColumns?: string[];
351
+ caseSensitive?: boolean;
352
+ trimWhitespace?: boolean;
353
+ }
354
+ interface CsvDuplicateGroup {
355
+ key: string[];
356
+ rowNumbers: number[];
357
+ count: number;
358
+ }
359
+ interface CsvDuplicateResult {
360
+ headers: string[];
361
+ totalRows: number;
362
+ duplicateGroups: CsvDuplicateGroup[];
363
+ duplicateRowCount: number;
364
+ uniqueRowCount: number;
365
+ }
366
+ /**
367
+ * Parse CSV/TSV data and group rows that share the same value across a
368
+ * chosen set of key columns (or the entire row, if no key columns are
369
+ * given), so you can spot and clean up accidental duplicates before an
370
+ * import.
371
+ */
372
+ declare function findCsvDuplicateRows(input: string, options?: CsvDuplicateOptions): CsvDuplicateResult | {
373
+ error: string;
374
+ };
346
375
 
347
376
  type data_ConvertedSubtitles = ConvertedSubtitles;
377
+ type data_CsvDuplicateGroup = CsvDuplicateGroup;
378
+ type data_CsvDuplicateOptions = CsvDuplicateOptions;
379
+ type data_CsvDuplicateResult = CsvDuplicateResult;
348
380
  type data_CsvParseResult = CsvParseResult;
349
381
  type data_CsvToSqlOptions = CsvToSqlOptions;
350
382
  type data_CsvToSqlResult = CsvToSqlResult;
@@ -377,6 +409,7 @@ declare const data_detectSubtitleFormat: typeof detectSubtitleFormat;
377
409
  declare const data_diffEnvExample: typeof diffEnvExample;
378
410
  declare const data_envToExport: typeof envToExport;
379
411
  declare const data_envToJson: typeof envToJson;
412
+ declare const data_findCsvDuplicateRows: typeof findCsvDuplicateRows;
380
413
  declare const data_formatNdjson: typeof formatNdjson;
381
414
  declare const data_formatSubtitleTimestamp: typeof formatSubtitleTimestamp;
382
415
  declare const data_formatToml: typeof formatToml;
@@ -416,7 +449,7 @@ declare const data_validateXml: typeof validateXml;
416
449
  declare const data_validateYaml: typeof validateYaml;
417
450
  declare const data_xmlToJson: typeof xmlToJson;
418
451
  declare namespace data {
419
- export { type data_ConvertedSubtitles as ConvertedSubtitles, type data_CsvParseResult as CsvParseResult, type data_CsvToSqlOptions as CsvToSqlOptions, type data_CsvToSqlResult as CsvToSqlResult, type data_EnvDiffError as EnvDiffError, type data_EnvDiffResult as EnvDiffResult, type data_EnvEntry as EnvEntry, type data_EnvParseResult as EnvParseResult, type data_FrontMatterError as FrontMatterError, type data_FrontMatterResult as FrontMatterResult, type data_IniSection as IniSection, type data_MarkdownTocEntry as MarkdownTocEntry, type data_MarkdownTocOptions as MarkdownTocOptions, type data_MarkdownTocResult as MarkdownTocResult, type data_NdjsonLineResult as NdjsonLineResult, type data_NdjsonValidateResult as NdjsonValidateResult, type data_ParseOptions as ParseOptions, type data_ParsedIni as ParsedIni, type data_ParsedSubtitles as ParsedSubtitles, type data_SubtitleCue as SubtitleCue, type data_SubtitleFormat as SubtitleFormat, type data_TomlFormatResult as TomlFormatResult, type data_ValidationResult as ValidationResult, type data_XmlFormatResult as XmlFormatResult, type data_XmlToJsonOptions as XmlToJsonOptions, data_convertSubtitles as convertSubtitles, data_csvToJson as csvToJson, data_csvToSqlInsert as csvToSqlInsert, data_detectDelimiter as detectDelimiter, data_detectSubtitleFormat as detectSubtitleFormat, data_diffEnvExample as diffEnvExample, data_envToExport as envToExport, data_envToJson as envToJson, data_formatNdjson as formatNdjson, data_formatSubtitleTimestamp as formatSubtitleTimestamp, data_formatToml as formatToml, data_formatXml as formatXml, data_generateMarkdownToc as generateMarkdownToc, data_getSection as getSection, data_getSectionNames as getSectionNames, data_getTomlStats as getTomlStats, data_getXmlStats as getXmlStats, data_getYamlStats as getYamlStats, data_iniToJson as iniToJson, data_jsonArrayToNdjson as jsonArrayToNdjson, data_jsonToIni as jsonToIni, data_jsonToToml as jsonToToml, data_jsonToXml as jsonToXml, data_minifyNdjson as minifyNdjson, data_minifyToml as minifyToml, data_minifyXml as minifyXml, data_ndjsonToJsonArray as ndjsonToJsonArray, data_parseCsv as parseCsv, data_parseEnv as parseEnv, data_parseFrontMatter as parseFrontMatter, data_parseIni as parseIni, data_parseSubtitleTimestamp as parseSubtitleTimestamp, data_parseSubtitles as parseSubtitles, data_parseXmlToObject as parseXmlToObject, data_serializeSubtitles as serializeSubtitles, data_shiftSubtitles as shiftSubtitles, data_stringifyIni as stringifyIni, data_toCsvString as toCsvString, data_tomlToJson as tomlToJson, data_validateEnvKey as validateEnvKey, data_validateIni as validateIni, data_validateNdjson as validateNdjson, data_validateToml as validateToml, data_validateXml as validateXml, data_validateYaml as validateYaml, data_xmlToJson as xmlToJson };
452
+ export { type data_ConvertedSubtitles as ConvertedSubtitles, type data_CsvDuplicateGroup as CsvDuplicateGroup, type data_CsvDuplicateOptions as CsvDuplicateOptions, type data_CsvDuplicateResult as CsvDuplicateResult, type data_CsvParseResult as CsvParseResult, type data_CsvToSqlOptions as CsvToSqlOptions, type data_CsvToSqlResult as CsvToSqlResult, type data_EnvDiffError as EnvDiffError, type data_EnvDiffResult as EnvDiffResult, type data_EnvEntry as EnvEntry, type data_EnvParseResult as EnvParseResult, type data_FrontMatterError as FrontMatterError, type data_FrontMatterResult as FrontMatterResult, type data_IniSection as IniSection, type data_MarkdownTocEntry as MarkdownTocEntry, type data_MarkdownTocOptions as MarkdownTocOptions, type data_MarkdownTocResult as MarkdownTocResult, type data_NdjsonLineResult as NdjsonLineResult, type data_NdjsonValidateResult as NdjsonValidateResult, type data_ParseOptions as ParseOptions, type data_ParsedIni as ParsedIni, type data_ParsedSubtitles as ParsedSubtitles, type data_SubtitleCue as SubtitleCue, type data_SubtitleFormat as SubtitleFormat, type data_TomlFormatResult as TomlFormatResult, type data_ValidationResult as ValidationResult, type data_XmlFormatResult as XmlFormatResult, type data_XmlToJsonOptions as XmlToJsonOptions, data_convertSubtitles as convertSubtitles, data_csvToJson as csvToJson, data_csvToSqlInsert as csvToSqlInsert, data_detectDelimiter as detectDelimiter, data_detectSubtitleFormat as detectSubtitleFormat, data_diffEnvExample as diffEnvExample, data_envToExport as envToExport, data_envToJson as envToJson, data_findCsvDuplicateRows as findCsvDuplicateRows, data_formatNdjson as formatNdjson, data_formatSubtitleTimestamp as formatSubtitleTimestamp, data_formatToml as formatToml, data_formatXml as formatXml, data_generateMarkdownToc as generateMarkdownToc, data_getSection as getSection, data_getSectionNames as getSectionNames, data_getTomlStats as getTomlStats, data_getXmlStats as getXmlStats, data_getYamlStats as getYamlStats, data_iniToJson as iniToJson, data_jsonArrayToNdjson as jsonArrayToNdjson, data_jsonToIni as jsonToIni, data_jsonToToml as jsonToToml, data_jsonToXml as jsonToXml, data_minifyNdjson as minifyNdjson, data_minifyToml as minifyToml, data_minifyXml as minifyXml, data_ndjsonToJsonArray as ndjsonToJsonArray, data_parseCsv as parseCsv, data_parseEnv as parseEnv, data_parseFrontMatter as parseFrontMatter, data_parseIni as parseIni, data_parseSubtitleTimestamp as parseSubtitleTimestamp, data_parseSubtitles as parseSubtitles, data_parseXmlToObject as parseXmlToObject, data_serializeSubtitles as serializeSubtitles, data_shiftSubtitles as shiftSubtitles, data_stringifyIni as stringifyIni, data_toCsvString as toCsvString, data_tomlToJson as tomlToJson, data_validateEnvKey as validateEnvKey, data_validateIni as validateIni, data_validateNdjson as validateNdjson, data_validateToml as validateToml, data_validateXml as validateXml, data_validateYaml as validateYaml, data_xmlToJson as xmlToJson };
420
453
  }
421
454
 
422
- export { parseCsv as $, formatXml as A, generateMarkdownToc as B, type ConvertedSubtitles as C, getSection as D, type EnvDiffError as E, type FrontMatterError as F, getSectionNames as G, getTomlStats as H, type IniSection as I, getXmlStats as J, getYamlStats as K, iniToJson as L, type MarkdownTocEntry as M, type NdjsonLineResult as N, jsonArrayToNdjson as O, type ParseOptions as P, jsonToIni as Q, jsonToToml as R, type SubtitleCue as S, type TomlFormatResult as T, jsonToXml as U, type ValidationResult as V, minifyNdjson as W, type XmlFormatResult as X, minifyToml as Y, minifyXml as Z, ndjsonToJsonArray as _, type CsvParseResult as a, parseEnv as a0, parseFrontMatter as a1, parseIni as a2, parseSubtitleTimestamp as a3, parseSubtitles as a4, parseXmlToObject as a5, serializeSubtitles as a6, shiftSubtitles as a7, stringifyIni as a8, toCsvString as a9, tomlToJson as aa, validateEnvKey as ab, validateIni as ac, validateNdjson as ad, validateToml as ae, validateXml as af, validateYaml as ag, xmlToJson as ah, type CsvToSqlOptions as b, type CsvToSqlResult as c, data as d, type EnvDiffResult as e, type EnvEntry as f, type EnvParseResult as g, type FrontMatterResult as h, type MarkdownTocOptions as i, type MarkdownTocResult as j, type NdjsonValidateResult as k, type ParsedIni as l, type ParsedSubtitles as m, type SubtitleFormat as n, type XmlToJsonOptions as o, convertSubtitles as p, csvToJson as q, csvToSqlInsert as r, detectDelimiter as s, detectSubtitleFormat as t, diffEnvExample as u, envToExport as v, envToJson as w, formatNdjson as x, formatSubtitleTimestamp as y, formatToml as z };
455
+ export { minifyNdjson as $, findCsvDuplicateRows as A, formatNdjson as B, type ConvertedSubtitles as C, formatSubtitleTimestamp as D, type EnvDiffError as E, type FrontMatterError as F, formatToml as G, formatXml as H, type IniSection as I, generateMarkdownToc as J, getSection as K, getSectionNames as L, type MarkdownTocEntry as M, type NdjsonLineResult as N, getTomlStats as O, type ParseOptions as P, getXmlStats as Q, getYamlStats as R, type SubtitleCue as S, type TomlFormatResult as T, iniToJson as U, type ValidationResult as V, jsonArrayToNdjson as W, type XmlFormatResult as X, jsonToIni as Y, jsonToToml as Z, jsonToXml as _, type CsvDuplicateGroup as a, minifyToml as a0, minifyXml as a1, ndjsonToJsonArray as a2, parseCsv as a3, parseEnv as a4, parseFrontMatter as a5, parseIni as a6, parseSubtitleTimestamp as a7, parseSubtitles as a8, parseXmlToObject as a9, serializeSubtitles as aa, shiftSubtitles as ab, stringifyIni as ac, toCsvString as ad, tomlToJson as ae, validateEnvKey as af, validateIni as ag, validateNdjson as ah, validateToml as ai, validateXml as aj, validateYaml as ak, xmlToJson as al, type CsvDuplicateOptions as b, type CsvDuplicateResult as c, data as d, type CsvParseResult as e, type CsvToSqlOptions as f, type CsvToSqlResult as g, type EnvDiffResult as h, type EnvEntry as i, type EnvParseResult as j, type FrontMatterResult as k, type MarkdownTocOptions as l, type MarkdownTocResult as m, type NdjsonValidateResult as n, type ParsedIni as o, type ParsedSubtitles as p, type SubtitleFormat as q, type XmlToJsonOptions as r, convertSubtitles as s, csvToJson as t, csvToSqlInsert as u, detectDelimiter as v, detectSubtitleFormat as w, diffEnvExample as x, envToExport as y, envToJson as z };