@utilix-tech/sdk 0.70.0 → 0.72.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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
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.72.0
6
+ - Add `validateVin` (vin-validator): validate a 17-character VIN via the ISO 3779 / NHTSA weighted mod-11 check digit and split it into its WMI, VDS, and VIS sections.
7
+ - Add `validateIsin` (isin-validator): validate a 12-character ISIN via its Luhn-style mod-10 check digit and break it into its country code and security identifier.
8
+
9
+ ## 0.71.0
10
+ - Add `readMidiInfo` (midi-file-info): parse a Standard MIDI File's header and every track chunk to report format, tick division, initial tempo, time signature, and per-track event/note-on counts, plus total duration in seconds accounting for tempo changes.
11
+
5
12
  ## 0.70.0
6
13
  - Add `generateHotp`, `generateTotp` (totp-hotp-generator): generate an HOTP code (RFC 4226) for a counter value, or the current TOTP code (RFC 6238) plus seconds until rotation, from a base32-encoded shared secret, supporting SHA-1/SHA-256/SHA-512 and 6-8 digit codes.
7
14
  - Add `calculateTaxEquivalentYield` (tax-equivalent-yield-calculator): convert between a tax-free municipal bond yield and its taxable equivalent given a marginal federal (plus optional state) tax rate, in both directions.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @utilix-tech/sdk
2
2
 
3
- **691 developer utility functions for Node.js: runs locally, no API key required.**
3
+ **694 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)
@@ -343,7 +343,7 @@ formatIso8601Duration(90061); // { iso: "P1DT1H1M1S", ... }
343
343
  ### `/units`: Conversions
344
344
 
345
345
  ```ts
346
- import { convertBytes, pxToAll, convert, formatValue, validateCardNumber, validateIban, calculateLoan, validateBarcode, calculateTax, calculateCompoundInterest, validateIsbn, calculateTipSplit, calculateSimpleInterest, calculateTimeValue, calculatePercentageChange, convertAprApy, calculateMarginMarkup, calculateDiscount, applySuccessiveDiscounts, calculateBreakEven, calculateVideoBitrate, calculateRuleOf72, calculateEmi, calculateRoi, compareUnitPrices, calculatePaybackPeriod, calculateCdMaturity, calculateDepreciationSchedule, calculateCashRounding, calculateMovingAverages, calculateGpa, calculateCreditUtilization, calculateDebtToIncomeRatio, calculateBlendedLoanRate, calculateFreelancerRate, calculateExtraPaymentPayoff, calculateLoanRefinance } from "@utilix-tech/sdk/units";
346
+ import { convertBytes, pxToAll, convert, formatValue, validateCardNumber, validateIban, calculateLoan, validateBarcode, calculateTax, calculateCompoundInterest, validateIsbn, calculateTipSplit, calculateSimpleInterest, calculateTimeValue, calculatePercentageChange, convertAprApy, calculateMarginMarkup, calculateDiscount, applySuccessiveDiscounts, calculateBreakEven, calculateVideoBitrate, calculateRuleOf72, calculateEmi, calculateRoi, compareUnitPrices, calculatePaybackPeriod, calculateCdMaturity, calculateDepreciationSchedule, calculateCashRounding, calculateMovingAverages, calculateGpa, calculateCreditUtilization, calculateDebtToIncomeRatio, calculateBlendedLoanRate, calculateFreelancerRate, calculateExtraPaymentPayoff, calculateLoanRefinance, validateVin, validateIsin } from "@utilix-tech/sdk/units";
347
347
 
348
348
  // File sizes
349
349
  convertBytes(1073741824, "GB"); // 1
@@ -501,6 +501,14 @@ planDebtPayoff([{ label: "Credit card", balance: 4000, ratePercent: 22, minPayme
501
501
  // Loan refinance break-even: monthly savings and the month closing costs are recouped
502
502
  calculateLoanRefinance(300000, 6.5, 300, 5.5, 300, 6000);
503
503
  // { monthlySavings: 183.36, breakEvenMonths: 33, currentTotalInterest: 307687.31, newTotalInterest: 252679.7, ... }
504
+
505
+ // VIN checksum (ISO 3779 / NHTSA) + WMI/VDS/VIS breakdown
506
+ validateVin("1HGCM82633A004352");
507
+ // { valid: true, checkDigit: "3", wmi: "1HG", vds: "CM8263", vis: "3A004352" }
508
+
509
+ // ISIN Luhn checksum (ISO 6166) + country code / security identifier breakdown
510
+ validateIsin("US0378331005");
511
+ // { valid: true, countryCode: "US", securityIdentifier: "037833100", checkDigit: 5, ... }
504
512
  ```
505
513
 
506
514
  ---
@@ -876,10 +884,10 @@ analyzeString("café"); // { length: 4, codePoints: [...], ... }
876
884
 
877
885
  ---
878
886
 
879
- ### `/media`: Image Header Parsing, EXIF, PDF Metadata & Page Dimensions, WAV, ID3, ICO, FLAC, Video & GIF Metadata
887
+ ### `/media`: Image Header Parsing, EXIF, PDF Metadata & Page Dimensions, WAV, ID3, ICO, FLAC, Video, GIF & MIDI Metadata
880
888
 
881
889
  ```ts
882
- import { readImageInfo, readExifData, readPdfMetadata, readPdfPageDimensions, readWavInfo, readId3Tags, readIcoInfo, readFlacInfo, readVideoInfo, inspectGifFrames } from "@utilix-tech/sdk/media";
890
+ import { readImageInfo, readExifData, readPdfMetadata, readPdfPageDimensions, readWavInfo, readId3Tags, readIcoInfo, readFlacInfo, readVideoInfo, inspectGifFrames, readMidiInfo } from "@utilix-tech/sdk/media";
883
891
 
884
892
  // Reads format, dimensions, bit depth, and alpha channel straight from
885
893
  // file bytes: no decoding, no canvas, no image library.
@@ -945,9 +953,17 @@ readVideoInfo(new Uint8Array(videoBytes));
945
953
  const gifBytes = await fs.promises.readFile("animation.gif");
946
954
  inspectGifFrames(new Uint8Array(gifBytes));
947
955
  // { version: "89a", width: 480, height: 270, frameCount: 24, animated: true, loopCount: 0, totalDurationMs: 2400, ... }
956
+
957
+ // Parses a Standard MIDI File's header and every track chunk to report
958
+ // format, tick division, initial tempo, time signature, and per-track
959
+ // event/note-on counts, plus total duration in seconds: no MIDI playback
960
+ // or synthesis.
961
+ const midiBytes = await fs.promises.readFile("song.mid");
962
+ readMidiInfo(new Uint8Array(midiBytes));
963
+ // { format: 1, trackCount: 2, initialTempoBpm: 120, timeSignature: { numerator: 4, denominator: 4 }, durationSeconds: 4, tracks: [...] }
948
964
  ```
949
965
 
950
- `readImageInfo` supports PNG, JPEG, GIF, WebP, and BMP. `readExifData` supports JPEG only. `readWavInfo` supports the canonical RIFF/WAVE container only. `readId3Tags` decodes ID3v2.3/2.4 and ID3v1/1.1 only (ID3v2.2 is detected but not decoded). `readIcoInfo` reads .ico icon files only and rejects .cur cursor files. `readFlacInfo` reads the native FLAC container only, not FLAC-in-Ogg. `readVideoInfo` supports MP4 (ISO BMFF) and WebM (EBML/Matroska) containers only. `inspectGifFrames` reads the GIF87a/89a block structure only; it never decodes pixel data.
966
+ `readImageInfo` supports PNG, JPEG, GIF, WebP, and BMP. `readExifData` supports JPEG only. `readWavInfo` supports the canonical RIFF/WAVE container only. `readId3Tags` decodes ID3v2.3/2.4 and ID3v1/1.1 only (ID3v2.2 is detected but not decoded). `readIcoInfo` reads .ico icon files only and rejects .cur cursor files. `readFlacInfo` reads the native FLAC container only, not FLAC-in-Ogg. `readVideoInfo` supports MP4 (ISO BMFF) and WebM (EBML/Matroska) containers only. `inspectGifFrames` reads the GIF87a/89a block structure only; it never decodes pixel data. `readMidiInfo` reads Standard MIDI Files (format 0/1/2) only.
951
967
 
952
968
  ---
953
969
 
@@ -9,6 +9,7 @@ __export(media_exports, {
9
9
  readIcoInfo: () => readIcoInfo,
10
10
  readId3Tags: () => readId3Tags,
11
11
  readImageInfo: () => readImageInfo,
12
+ readMidiInfo: () => readMidiInfo,
12
13
  readPdfMetadata: () => readPdfMetadata,
13
14
  readPdfPageDimensions: () => readPdfPageDimensions,
14
15
  readVideoInfo: () => readVideoInfo,
@@ -1306,5 +1307,135 @@ function inspectGifFrames(bytes) {
1306
1307
  hasTransparency
1307
1308
  };
1308
1309
  }
1310
+ function readU16BE(bytes, offset) {
1311
+ return bytes[offset] << 8 | bytes[offset + 1];
1312
+ }
1313
+ function readMidiVarLen(bytes, pos) {
1314
+ let value = 0;
1315
+ let i = pos;
1316
+ for (let n = 0; n < 4; n++) {
1317
+ if (i >= bytes.length) return null;
1318
+ const b = bytes[i];
1319
+ value = value << 7 | b & 127;
1320
+ i++;
1321
+ if ((b & 128) === 0) return { value, next: i };
1322
+ }
1323
+ return null;
1324
+ }
1325
+ function readMidiInfo(bytes) {
1326
+ if (bytes.length < 14) return { error: "File is too small to be a valid MIDI file" };
1327
+ if (Buffer.from(bytes.subarray(0, 4)).toString("ascii") !== "MThd") {
1328
+ return { error: 'Not a valid MIDI file (missing "MThd" header chunk)' };
1329
+ }
1330
+ const headerLength = readU32BE(bytes, 4);
1331
+ if (headerLength < 6 || 8 + headerLength > bytes.length) return { error: "Truncated or invalid MThd header chunk" };
1332
+ const format = readU16BE(bytes, 8);
1333
+ if (format !== 0 && format !== 1 && format !== 2) return { error: `Unsupported MIDI format: ${format} (expected 0, 1, or 2)` };
1334
+ const trackCount = readU16BE(bytes, 10);
1335
+ if (trackCount < 1) return { error: "MIDI file declares zero tracks" };
1336
+ const divisionRaw = readU16BE(bytes, 12);
1337
+ const division = (divisionRaw & 32768) !== 0 ? { type: "smpte", framesPerSecond: -(divisionRaw >> 8 << 24 >> 24), ticksPerFrame: divisionRaw & 255 } : { type: "ppq", ticksPerQuarterNote: divisionRaw & 32767 };
1338
+ const tracks = [];
1339
+ const tempoChanges = [];
1340
+ let timeSignature = null;
1341
+ let maxEndTick = 0;
1342
+ let offset = 8 + headerLength;
1343
+ let tracksParsed = 0;
1344
+ while (tracksParsed < trackCount && offset + 8 <= bytes.length) {
1345
+ const chunkId = Buffer.from(bytes.subarray(offset, offset + 4)).toString("ascii");
1346
+ const chunkLength = readU32BE(bytes, offset + 4);
1347
+ const dataStart = offset + 8;
1348
+ const dataEnd = Math.min(dataStart + chunkLength, bytes.length);
1349
+ if (chunkId !== "MTrk") {
1350
+ offset = dataEnd;
1351
+ continue;
1352
+ }
1353
+ let pos = dataStart;
1354
+ let runningStatus = 0;
1355
+ let absoluteTick = 0;
1356
+ let eventCount = 0;
1357
+ let noteOnCount = 0;
1358
+ let name = null;
1359
+ while (pos < dataEnd) {
1360
+ const delta = readMidiVarLen(bytes, pos);
1361
+ if (!delta) break;
1362
+ pos = delta.next;
1363
+ absoluteTick += delta.value;
1364
+ if (pos >= dataEnd) break;
1365
+ let statusByte = bytes[pos];
1366
+ if (statusByte & 128) {
1367
+ runningStatus = statusByte;
1368
+ pos++;
1369
+ }
1370
+ statusByte = runningStatus;
1371
+ eventCount++;
1372
+ if (statusByte === 255) {
1373
+ const metaType = bytes[pos];
1374
+ pos++;
1375
+ const len = readMidiVarLen(bytes, pos);
1376
+ if (!len) break;
1377
+ pos = len.next;
1378
+ const data = bytes.subarray(pos, Math.min(pos + len.value, dataEnd));
1379
+ pos += len.value;
1380
+ if (metaType === 81 && data.length >= 3) {
1381
+ tempoChanges.push({ tick: absoluteTick, usPerQuarter: data[0] << 16 | data[1] << 8 | data[2] });
1382
+ } else if (metaType === 88 && data.length >= 2) {
1383
+ if (!timeSignature) timeSignature = { numerator: data[0], denominator: 2 ** data[1] };
1384
+ } else if (metaType === 3 && name === null) {
1385
+ name = Buffer.from(data).toString("utf8").trim() || null;
1386
+ } else if (metaType === 47) {
1387
+ break;
1388
+ }
1389
+ } else if (statusByte === 240 || statusByte === 247) {
1390
+ const len = readMidiVarLen(bytes, pos);
1391
+ if (!len) break;
1392
+ pos = len.next + len.value;
1393
+ } else if (statusByte >= 128) {
1394
+ const type = statusByte & 240;
1395
+ if (type === 192 || type === 208) {
1396
+ pos += 1;
1397
+ } else {
1398
+ if (type === 144 && (bytes[pos + 1] ?? 0) > 0) noteOnCount++;
1399
+ pos += 2;
1400
+ }
1401
+ } else {
1402
+ break;
1403
+ }
1404
+ }
1405
+ tracks.push({ index: tracksParsed, name, eventCount, noteOnCount });
1406
+ maxEndTick = Math.max(maxEndTick, absoluteTick);
1407
+ offset = dataEnd;
1408
+ tracksParsed++;
1409
+ }
1410
+ if (tracks.length === 0) return { error: "No MTrk track chunks found in this MIDI file" };
1411
+ const DEFAULT_US_PER_QUARTER = 5e5;
1412
+ const initialTempoBpm = Math.round(6e7 / (tempoChanges[0]?.usPerQuarter ?? DEFAULT_US_PER_QUARTER) * 100) / 100;
1413
+ let durationSeconds = null;
1414
+ if (division.type === "smpte") {
1415
+ const ticksPerSecond = division.framesPerSecond * division.ticksPerFrame;
1416
+ durationSeconds = ticksPerSecond > 0 ? Math.round(maxEndTick / ticksPerSecond * 1e3) / 1e3 : null;
1417
+ } else {
1418
+ const sorted = [...tempoChanges].sort((a, b) => a.tick - b.tick);
1419
+ let seconds = 0;
1420
+ let prevTick = 0;
1421
+ let currentUsPerQuarter = DEFAULT_US_PER_QUARTER;
1422
+ for (const change of sorted) {
1423
+ seconds += (change.tick - prevTick) / division.ticksPerQuarterNote * (currentUsPerQuarter / 1e6);
1424
+ prevTick = change.tick;
1425
+ currentUsPerQuarter = change.usPerQuarter;
1426
+ }
1427
+ seconds += (maxEndTick - prevTick) / division.ticksPerQuarterNote * (currentUsPerQuarter / 1e6);
1428
+ durationSeconds = Math.round(seconds * 1e3) / 1e3;
1429
+ }
1430
+ return {
1431
+ format,
1432
+ trackCount: tracks.length,
1433
+ division,
1434
+ initialTempoBpm,
1435
+ timeSignature,
1436
+ durationSeconds,
1437
+ tracks
1438
+ };
1439
+ }
1309
1440
 
1310
- export { inspectGifFrames, media_exports, readExifData, readFlacInfo, readIcoInfo, readId3Tags, readImageInfo, readPdfMetadata, readPdfPageDimensions, readVideoInfo, readWavInfo };
1441
+ export { inspectGifFrames, media_exports, readExifData, readFlacInfo, readIcoInfo, readId3Tags, readImageInfo, readMidiInfo, readPdfMetadata, readPdfPageDimensions, readVideoInfo, readWavInfo };
@@ -104,6 +104,8 @@ __export(units_exports, {
104
104
  validateCardNumber: () => validateCardNumber,
105
105
  validateIban: () => validateIban,
106
106
  validateIsbn: () => validateIsbn,
107
+ validateIsin: () => validateIsin,
108
+ validateVin: () => validateVin,
107
109
  viewportToPx: () => viewportToPx
108
110
  });
109
111
  function pxToAll(px, baseFontSize = 16, viewportWidth = 1440, viewportHeight = 900) {
@@ -2543,5 +2545,93 @@ function calculateTaxEquivalentYield(inputYieldPercent, federalTaxRatePercent, s
2543
2545
  yieldDifference: round6(taxableYield - taxFreeYield)
2544
2546
  };
2545
2547
  }
2548
+ var VIN_TRANSLITERATION = {
2549
+ A: 1,
2550
+ B: 2,
2551
+ C: 3,
2552
+ D: 4,
2553
+ E: 5,
2554
+ F: 6,
2555
+ G: 7,
2556
+ H: 8,
2557
+ J: 1,
2558
+ K: 2,
2559
+ L: 3,
2560
+ M: 4,
2561
+ N: 5,
2562
+ P: 7,
2563
+ R: 9,
2564
+ S: 2,
2565
+ T: 3,
2566
+ U: 4,
2567
+ V: 5,
2568
+ W: 6,
2569
+ X: 7,
2570
+ Y: 8,
2571
+ Z: 9
2572
+ };
2573
+ var VIN_WEIGHTS = [8, 7, 6, 5, 4, 3, 2, 10, 0, 9, 8, 7, 6, 5, 4, 3, 2];
2574
+ function validateVin(input) {
2575
+ if (!input || !input.trim()) return { error: "Input is empty" };
2576
+ const cleaned = input.replace(/\s/g, "").toUpperCase();
2577
+ if (cleaned.length !== 17) return { error: `VIN length (${cleaned.length}) must be exactly 17 characters` };
2578
+ if (!/^[A-HJ-NPR-Z0-9]{17}$/.test(cleaned)) {
2579
+ return { error: "VIN must contain only letters and digits, excluding I, O, and Q" };
2580
+ }
2581
+ let sum = 0;
2582
+ for (let i = 0; i < 17; i++) {
2583
+ const ch = cleaned[i];
2584
+ const value = /[0-9]/.test(ch) ? Number(ch) : VIN_TRANSLITERATION[ch];
2585
+ sum += value * VIN_WEIGHTS[i];
2586
+ }
2587
+ const remainder = sum % 11;
2588
+ const expectedCheckDigit = remainder === 10 ? "X" : String(remainder);
2589
+ const checkDigit = cleaned[8];
2590
+ return {
2591
+ valid: checkDigit === expectedCheckDigit,
2592
+ vin: cleaned,
2593
+ checkDigit,
2594
+ expectedCheckDigit,
2595
+ wmi: cleaned.slice(0, 3),
2596
+ vds: cleaned.slice(3, 9),
2597
+ vis: cleaned.slice(9, 17)
2598
+ };
2599
+ }
2600
+ function isinLuhnCheckDigit(payloadDigits) {
2601
+ let sum = 0;
2602
+ let shouldDouble = true;
2603
+ for (let i = payloadDigits.length - 1; i >= 0; i--) {
2604
+ let d = parseInt(payloadDigits[i], 10);
2605
+ if (shouldDouble) {
2606
+ d *= 2;
2607
+ if (d > 9) d -= 9;
2608
+ }
2609
+ sum += d;
2610
+ shouldDouble = !shouldDouble;
2611
+ }
2612
+ return (10 - sum % 10) % 10;
2613
+ }
2614
+ function validateIsin(input) {
2615
+ if (!input || !input.trim()) return { error: "Input is empty" };
2616
+ const cleaned = input.replace(/\s/g, "").toUpperCase();
2617
+ if (cleaned.length !== 12) return { error: `ISIN length (${cleaned.length}) must be exactly 12 characters` };
2618
+ if (!/^[A-Z]{2}[A-Z0-9]{9}\d$/.test(cleaned)) {
2619
+ return { error: "ISIN must be a 2-letter country code, 9 alphanumeric characters, then 1 numeric check digit" };
2620
+ }
2621
+ const countryCode = cleaned.slice(0, 2);
2622
+ const securityIdentifier = cleaned.slice(2, 11);
2623
+ const checkDigit = Number(cleaned[11]);
2624
+ const payload = cleaned.slice(0, 11).split("").map((ch) => /[A-Z]/.test(ch) ? String(ch.charCodeAt(0) - 55) : ch).join("");
2625
+ const expectedCheckDigit = isinLuhnCheckDigit(payload);
2626
+ return {
2627
+ valid: checkDigit === expectedCheckDigit,
2628
+ isin: cleaned,
2629
+ countryCode,
2630
+ securityIdentifier,
2631
+ checkDigit,
2632
+ expectedCheckDigit,
2633
+ formatted: `${countryCode} ${securityIdentifier} ${checkDigit}`
2634
+ };
2635
+ }
2546
2636
 
2547
- 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, GPA_GRADE_POINTS, USD_DENOMINATIONS, applySuccessiveDiscounts, autoFormat, bumpVersion, calcAspectRatio, calcSubnet, calculateBlendedLoanRate, calculateBreakEven, calculateCashBreakdown, calculateCashRounding, calculateCdMaturity, calculateChange, calculateCompoundInterest, calculateCreditUtilization, calculateDebtToIncomeRatio, calculateDepreciationSchedule, calculateDiscount, calculateEmi, calculateExtraPaymentPayoff, calculateFreelancerRate, calculateGpa, calculateLoan, calculateLoanRefinance, calculateMarginMarkup, calculateMovingAverages, calculatePaybackPeriod, calculatePercentageChange, calculateRoi, calculateRuleOf72, calculateSavingsGoal, calculateSimpleInterest, calculateTax, calculateTaxEquivalentYield, 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, planDebtPayoff, ptToPx, pxToAll, pxToViewport, remToPx, satisfiesRange, scaleByHeight, scaleByWidth, sortVersions, splitSubnet, symbolicToPermissions, units_exports, validateBarcode, validateCardNumber, validateIban, validateIsbn, viewportToPx };
2637
+ 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, GPA_GRADE_POINTS, USD_DENOMINATIONS, applySuccessiveDiscounts, autoFormat, bumpVersion, calcAspectRatio, calcSubnet, calculateBlendedLoanRate, calculateBreakEven, calculateCashBreakdown, calculateCashRounding, calculateCdMaturity, calculateChange, calculateCompoundInterest, calculateCreditUtilization, calculateDebtToIncomeRatio, calculateDepreciationSchedule, calculateDiscount, calculateEmi, calculateExtraPaymentPayoff, calculateFreelancerRate, calculateGpa, calculateLoan, calculateLoanRefinance, calculateMarginMarkup, calculateMovingAverages, calculatePaybackPeriod, calculatePercentageChange, calculateRoi, calculateRuleOf72, calculateSavingsGoal, calculateSimpleInterest, calculateTax, calculateTaxEquivalentYield, 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, planDebtPayoff, ptToPx, pxToAll, pxToViewport, remToPx, satisfiesRange, scaleByHeight, scaleByWidth, sortVersions, splitSubnet, symbolicToPermissions, units_exports, validateBarcode, validateCardNumber, validateIban, validateIsbn, validateIsin, validateVin, viewportToPx };
package/dist/index.cjs CHANGED
@@ -8638,6 +8638,8 @@ __export(units_exports, {
8638
8638
  validateCardNumber: () => validateCardNumber,
8639
8639
  validateIban: () => validateIban,
8640
8640
  validateIsbn: () => validateIsbn,
8641
+ validateIsin: () => validateIsin,
8642
+ validateVin: () => validateVin,
8641
8643
  viewportToPx: () => viewportToPx
8642
8644
  });
8643
8645
  function pxToAll(px, baseFontSize = 16, viewportWidth = 1440, viewportHeight = 900) {
@@ -11077,6 +11079,94 @@ function calculateTaxEquivalentYield(inputYieldPercent, federalTaxRatePercent, s
11077
11079
  yieldDifference: round6(taxableYield - taxFreeYield)
11078
11080
  };
11079
11081
  }
11082
+ var VIN_TRANSLITERATION = {
11083
+ A: 1,
11084
+ B: 2,
11085
+ C: 3,
11086
+ D: 4,
11087
+ E: 5,
11088
+ F: 6,
11089
+ G: 7,
11090
+ H: 8,
11091
+ J: 1,
11092
+ K: 2,
11093
+ L: 3,
11094
+ M: 4,
11095
+ N: 5,
11096
+ P: 7,
11097
+ R: 9,
11098
+ S: 2,
11099
+ T: 3,
11100
+ U: 4,
11101
+ V: 5,
11102
+ W: 6,
11103
+ X: 7,
11104
+ Y: 8,
11105
+ Z: 9
11106
+ };
11107
+ var VIN_WEIGHTS = [8, 7, 6, 5, 4, 3, 2, 10, 0, 9, 8, 7, 6, 5, 4, 3, 2];
11108
+ function validateVin(input) {
11109
+ if (!input || !input.trim()) return { error: "Input is empty" };
11110
+ const cleaned = input.replace(/\s/g, "").toUpperCase();
11111
+ if (cleaned.length !== 17) return { error: `VIN length (${cleaned.length}) must be exactly 17 characters` };
11112
+ if (!/^[A-HJ-NPR-Z0-9]{17}$/.test(cleaned)) {
11113
+ return { error: "VIN must contain only letters and digits, excluding I, O, and Q" };
11114
+ }
11115
+ let sum = 0;
11116
+ for (let i = 0; i < 17; i++) {
11117
+ const ch = cleaned[i];
11118
+ const value = /[0-9]/.test(ch) ? Number(ch) : VIN_TRANSLITERATION[ch];
11119
+ sum += value * VIN_WEIGHTS[i];
11120
+ }
11121
+ const remainder = sum % 11;
11122
+ const expectedCheckDigit = remainder === 10 ? "X" : String(remainder);
11123
+ const checkDigit = cleaned[8];
11124
+ return {
11125
+ valid: checkDigit === expectedCheckDigit,
11126
+ vin: cleaned,
11127
+ checkDigit,
11128
+ expectedCheckDigit,
11129
+ wmi: cleaned.slice(0, 3),
11130
+ vds: cleaned.slice(3, 9),
11131
+ vis: cleaned.slice(9, 17)
11132
+ };
11133
+ }
11134
+ function isinLuhnCheckDigit(payloadDigits) {
11135
+ let sum = 0;
11136
+ let shouldDouble = true;
11137
+ for (let i = payloadDigits.length - 1; i >= 0; i--) {
11138
+ let d = parseInt(payloadDigits[i], 10);
11139
+ if (shouldDouble) {
11140
+ d *= 2;
11141
+ if (d > 9) d -= 9;
11142
+ }
11143
+ sum += d;
11144
+ shouldDouble = !shouldDouble;
11145
+ }
11146
+ return (10 - sum % 10) % 10;
11147
+ }
11148
+ function validateIsin(input) {
11149
+ if (!input || !input.trim()) return { error: "Input is empty" };
11150
+ const cleaned = input.replace(/\s/g, "").toUpperCase();
11151
+ if (cleaned.length !== 12) return { error: `ISIN length (${cleaned.length}) must be exactly 12 characters` };
11152
+ if (!/^[A-Z]{2}[A-Z0-9]{9}\d$/.test(cleaned)) {
11153
+ return { error: "ISIN must be a 2-letter country code, 9 alphanumeric characters, then 1 numeric check digit" };
11154
+ }
11155
+ const countryCode = cleaned.slice(0, 2);
11156
+ const securityIdentifier = cleaned.slice(2, 11);
11157
+ const checkDigit = Number(cleaned[11]);
11158
+ const payload = cleaned.slice(0, 11).split("").map((ch) => /[A-Z]/.test(ch) ? String(ch.charCodeAt(0) - 55) : ch).join("");
11159
+ const expectedCheckDigit = isinLuhnCheckDigit(payload);
11160
+ return {
11161
+ valid: checkDigit === expectedCheckDigit,
11162
+ isin: cleaned,
11163
+ countryCode,
11164
+ securityIdentifier,
11165
+ checkDigit,
11166
+ expectedCheckDigit,
11167
+ formatted: `${countryCode} ${securityIdentifier} ${checkDigit}`
11168
+ };
11169
+ }
11080
11170
 
11081
11171
  // src/tools/network.ts
11082
11172
  var network_exports = {};
@@ -24357,6 +24447,7 @@ __export(media_exports, {
24357
24447
  readIcoInfo: () => readIcoInfo,
24358
24448
  readId3Tags: () => readId3Tags,
24359
24449
  readImageInfo: () => readImageInfo,
24450
+ readMidiInfo: () => readMidiInfo,
24360
24451
  readPdfMetadata: () => readPdfMetadata,
24361
24452
  readPdfPageDimensions: () => readPdfPageDimensions,
24362
24453
  readVideoInfo: () => readVideoInfo,
@@ -25654,6 +25745,136 @@ function inspectGifFrames(bytes) {
25654
25745
  hasTransparency
25655
25746
  };
25656
25747
  }
25748
+ function readU16BE(bytes, offset) {
25749
+ return bytes[offset] << 8 | bytes[offset + 1];
25750
+ }
25751
+ function readMidiVarLen(bytes, pos) {
25752
+ let value = 0;
25753
+ let i = pos;
25754
+ for (let n = 0; n < 4; n++) {
25755
+ if (i >= bytes.length) return null;
25756
+ const b = bytes[i];
25757
+ value = value << 7 | b & 127;
25758
+ i++;
25759
+ if ((b & 128) === 0) return { value, next: i };
25760
+ }
25761
+ return null;
25762
+ }
25763
+ function readMidiInfo(bytes) {
25764
+ if (bytes.length < 14) return { error: "File is too small to be a valid MIDI file" };
25765
+ if (Buffer.from(bytes.subarray(0, 4)).toString("ascii") !== "MThd") {
25766
+ return { error: 'Not a valid MIDI file (missing "MThd" header chunk)' };
25767
+ }
25768
+ const headerLength = readU32BE(bytes, 4);
25769
+ if (headerLength < 6 || 8 + headerLength > bytes.length) return { error: "Truncated or invalid MThd header chunk" };
25770
+ const format = readU16BE(bytes, 8);
25771
+ if (format !== 0 && format !== 1 && format !== 2) return { error: `Unsupported MIDI format: ${format} (expected 0, 1, or 2)` };
25772
+ const trackCount = readU16BE(bytes, 10);
25773
+ if (trackCount < 1) return { error: "MIDI file declares zero tracks" };
25774
+ const divisionRaw = readU16BE(bytes, 12);
25775
+ const division = (divisionRaw & 32768) !== 0 ? { type: "smpte", framesPerSecond: -(divisionRaw >> 8 << 24 >> 24), ticksPerFrame: divisionRaw & 255 } : { type: "ppq", ticksPerQuarterNote: divisionRaw & 32767 };
25776
+ const tracks = [];
25777
+ const tempoChanges = [];
25778
+ let timeSignature = null;
25779
+ let maxEndTick = 0;
25780
+ let offset = 8 + headerLength;
25781
+ let tracksParsed = 0;
25782
+ while (tracksParsed < trackCount && offset + 8 <= bytes.length) {
25783
+ const chunkId = Buffer.from(bytes.subarray(offset, offset + 4)).toString("ascii");
25784
+ const chunkLength = readU32BE(bytes, offset + 4);
25785
+ const dataStart = offset + 8;
25786
+ const dataEnd = Math.min(dataStart + chunkLength, bytes.length);
25787
+ if (chunkId !== "MTrk") {
25788
+ offset = dataEnd;
25789
+ continue;
25790
+ }
25791
+ let pos = dataStart;
25792
+ let runningStatus = 0;
25793
+ let absoluteTick = 0;
25794
+ let eventCount = 0;
25795
+ let noteOnCount = 0;
25796
+ let name = null;
25797
+ while (pos < dataEnd) {
25798
+ const delta = readMidiVarLen(bytes, pos);
25799
+ if (!delta) break;
25800
+ pos = delta.next;
25801
+ absoluteTick += delta.value;
25802
+ if (pos >= dataEnd) break;
25803
+ let statusByte = bytes[pos];
25804
+ if (statusByte & 128) {
25805
+ runningStatus = statusByte;
25806
+ pos++;
25807
+ }
25808
+ statusByte = runningStatus;
25809
+ eventCount++;
25810
+ if (statusByte === 255) {
25811
+ const metaType = bytes[pos];
25812
+ pos++;
25813
+ const len = readMidiVarLen(bytes, pos);
25814
+ if (!len) break;
25815
+ pos = len.next;
25816
+ const data = bytes.subarray(pos, Math.min(pos + len.value, dataEnd));
25817
+ pos += len.value;
25818
+ if (metaType === 81 && data.length >= 3) {
25819
+ tempoChanges.push({ tick: absoluteTick, usPerQuarter: data[0] << 16 | data[1] << 8 | data[2] });
25820
+ } else if (metaType === 88 && data.length >= 2) {
25821
+ if (!timeSignature) timeSignature = { numerator: data[0], denominator: 2 ** data[1] };
25822
+ } else if (metaType === 3 && name === null) {
25823
+ name = Buffer.from(data).toString("utf8").trim() || null;
25824
+ } else if (metaType === 47) {
25825
+ break;
25826
+ }
25827
+ } else if (statusByte === 240 || statusByte === 247) {
25828
+ const len = readMidiVarLen(bytes, pos);
25829
+ if (!len) break;
25830
+ pos = len.next + len.value;
25831
+ } else if (statusByte >= 128) {
25832
+ const type = statusByte & 240;
25833
+ if (type === 192 || type === 208) {
25834
+ pos += 1;
25835
+ } else {
25836
+ if (type === 144 && (bytes[pos + 1] ?? 0) > 0) noteOnCount++;
25837
+ pos += 2;
25838
+ }
25839
+ } else {
25840
+ break;
25841
+ }
25842
+ }
25843
+ tracks.push({ index: tracksParsed, name, eventCount, noteOnCount });
25844
+ maxEndTick = Math.max(maxEndTick, absoluteTick);
25845
+ offset = dataEnd;
25846
+ tracksParsed++;
25847
+ }
25848
+ if (tracks.length === 0) return { error: "No MTrk track chunks found in this MIDI file" };
25849
+ const DEFAULT_US_PER_QUARTER = 5e5;
25850
+ const initialTempoBpm = Math.round(6e7 / (tempoChanges[0]?.usPerQuarter ?? DEFAULT_US_PER_QUARTER) * 100) / 100;
25851
+ let durationSeconds = null;
25852
+ if (division.type === "smpte") {
25853
+ const ticksPerSecond = division.framesPerSecond * division.ticksPerFrame;
25854
+ durationSeconds = ticksPerSecond > 0 ? Math.round(maxEndTick / ticksPerSecond * 1e3) / 1e3 : null;
25855
+ } else {
25856
+ const sorted = [...tempoChanges].sort((a, b) => a.tick - b.tick);
25857
+ let seconds = 0;
25858
+ let prevTick = 0;
25859
+ let currentUsPerQuarter = DEFAULT_US_PER_QUARTER;
25860
+ for (const change of sorted) {
25861
+ seconds += (change.tick - prevTick) / division.ticksPerQuarterNote * (currentUsPerQuarter / 1e6);
25862
+ prevTick = change.tick;
25863
+ currentUsPerQuarter = change.usPerQuarter;
25864
+ }
25865
+ seconds += (maxEndTick - prevTick) / division.ticksPerQuarterNote * (currentUsPerQuarter / 1e6);
25866
+ durationSeconds = Math.round(seconds * 1e3) / 1e3;
25867
+ }
25868
+ return {
25869
+ format,
25870
+ trackCount: tracks.length,
25871
+ division,
25872
+ initialTempoBpm,
25873
+ timeSignature,
25874
+ durationSeconds,
25875
+ tracks
25876
+ };
25877
+ }
25657
25878
 
25658
25879
  // src/index.ts
25659
25880
  var version = "0.1.0";
package/dist/index.d.cts CHANGED
@@ -5,7 +5,7 @@ export { t as text } from './text-DWUcY8H1.cjs';
5
5
  export { d as data } from './data-26TALa9U.cjs';
6
6
  export { g as generators } from './generators-DAXXrMvG.cjs';
7
7
  export { t as time } from './time-KqJjdqII.cjs';
8
- export { u as units } from './units-BT8P27G0.cjs';
8
+ export { u as units } from './units-DB0W2pvU.cjs';
9
9
  export { n as network } from './network-BmqYmfqY.cjs';
10
10
  export { a as api } from './api-C8rImfka.cjs';
11
11
  export { c as code } from './code-CoeD4Fmq.cjs';
@@ -13,7 +13,7 @@ export { c as color } from './color-tPwZCr9H.cjs';
13
13
  export { c as css } from './css-Cf7AMGM-.cjs';
14
14
  export { m as misc } from './misc-CA3N198T.cjs';
15
15
  export { a as ai_agent } from './ai_agent-CmrVjvYH.cjs';
16
- export { m as media } from './media-BUvwm4wU.cjs';
16
+ export { m as media } from './media-56zrIBw4.cjs';
17
17
 
18
18
  declare const version = "0.1.0";
19
19
 
package/dist/index.d.ts CHANGED
@@ -5,7 +5,7 @@ export { t as text } from './text-DWUcY8H1.js';
5
5
  export { d as data } from './data-26TALa9U.js';
6
6
  export { g as generators } from './generators-DAXXrMvG.js';
7
7
  export { t as time } from './time-KqJjdqII.js';
8
- export { u as units } from './units-BT8P27G0.js';
8
+ export { u as units } from './units-DB0W2pvU.js';
9
9
  export { n as network } from './network-BmqYmfqY.js';
10
10
  export { a as api } from './api-C8rImfka.js';
11
11
  export { c as code } from './code-CoeD4Fmq.js';
@@ -13,7 +13,7 @@ export { c as color } from './color-tPwZCr9H.js';
13
13
  export { c as css } from './css-Cf7AMGM-.js';
14
14
  export { m as misc } from './misc-CA3N198T.js';
15
15
  export { a as ai_agent } from './ai_agent-CmrVjvYH.js';
16
- export { m as media } from './media-BUvwm4wU.js';
16
+ export { m as media } from './media-56zrIBw4.js';
17
17
 
18
18
  declare const version = "0.1.0";
19
19
 
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- export { media_exports as media } from './chunk-226EH3ZI.js';
2
- export { units_exports as units } from './chunk-653HDWXW.js';
1
+ export { media_exports as media } from './chunk-PT4N4UBL.js';
2
+ export { units_exports as units } from './chunk-S5FTESKL.js';
3
3
  export { network_exports as network } from './chunk-ACGCCOCK.js';
4
4
  export { api_exports as api } from './chunk-4S63T7Y4.js';
5
5
  export { code_exports as code } from './chunk-DAD2ZOTJ.js';