@utilix-tech/sdk 0.97.0 → 0.98.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,10 @@
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.98.0
6
+ - Add `convertTimecode` (timecode-converter): convert between a frame count, SMPTE timecode (HH:MM:SS:FF), and seconds at a given frame rate, handling NTSC drop-frame timecode's skipped frame numbers correctly.
7
+ - Add `validateGeoJson` (geojson-validator): validate a GeoJSON document against RFC 7946's structural rules, geometry type/coordinates shape, polygon ring closure and winding, and a FeatureCollection's required fields.
8
+
5
9
  ## 0.97.0
6
10
  - Add `calculatePositionSize` (position-size-risk-calculator): compute a trade's position size from account size, risk percent per trade, entry price, and stop-loss price, plus the risk/reward ratio against an optional target price.
7
11
  - Add `detectWeaselWords` (weasel-word-detector): scan text for weasel words, vague hedging phrases that imply evidence or consensus without naming a checkable source, like "studies show" or "many experts believe".
package/README.md CHANGED
@@ -67,7 +67,7 @@ const { formatJson } = require("@utilix-tech/sdk/json");
67
67
  ```ts
68
68
  import { formatJson, minifyJson, diffJson, jsonToTypescript, jsonSchemaToTypescript, validateJson,
69
69
  evaluateJsonPath, resolveJsonPointer, applyJsonMergePatch, diffJsonPatch, applyJsonPatch,
70
- jsonToCsv, yamlToJson, jsonToYaml, getJsonStats } from "@utilix-tech/sdk/json";
70
+ jsonToCsv, yamlToJson, jsonToYaml, getJsonStats, validateGeoJson } from "@utilix-tech/sdk/json";
71
71
 
72
72
  // Format with 2-space indent
73
73
  formatJson('{"name":"alice","age":30}', { indent: 2 });
@@ -103,6 +103,10 @@ jsonToCsv('[{"a":1,"b":2}]');
103
103
 
104
104
  // Validate against JSON Schema
105
105
  validateJson(data, schema);
106
+
107
+ // Validate a GeoJSON document's structure against RFC 7946
108
+ validateGeoJson('{"type":"Point","coordinates":[30,10]}');
109
+ // { valid: true, type: "Point", errors: [], warnings: [] }
106
110
  ```
107
111
 
108
112
  ---
@@ -463,6 +467,10 @@ calculateVideoBitrate({ mode: "size-from-bitrate", durationSeconds: 600, videoBi
463
467
  calculateRuleOf72(8, "rate-to-years");
464
468
  // { ruleOf72Estimate: 9, exactValue: 9.0065, errorPercent: -0.0718, ... }
465
469
 
470
+ // Frame count <-> SMPTE timecode <-> seconds, with correct NTSC drop-frame handling
471
+ convertTimecode({ mode: "frames-to-timecode", frameRate: 29.97, dropFrame: true, frameCount: 17982 });
472
+ // { timecode: "00:10:00;00", frameCount: 17982, seconds: 600 }
473
+
466
474
  // EMI: equated monthly installment with a year-by-year principal/interest breakdown
467
475
  calculateEmi(500000, 8.5, 5, "years");
468
476
  // { emi: 10258.27, totalInterest: 115495.89, tenureMonths: 60, yearlyBreakdown: [...] }
@@ -33,6 +33,7 @@ __export(json_exports, {
33
33
  resolveJsonPointer: () => resolveJsonPointer,
34
34
  toPascalCase: () => toPascalCase,
35
35
  toSnakeCase: () => toSnakeCase,
36
+ validateGeoJson: () => validateGeoJson,
36
37
  validateJson: () => validateJson,
37
38
  validateJsonPath: () => validateJsonPath,
38
39
  validateJsonString: () => validateJsonString,
@@ -1651,5 +1652,170 @@ function generateJsonPatch(aJson, bJson) {
1651
1652
  }
1652
1653
  return { patch: diffJsonPatch(a, b) };
1653
1654
  }
1655
+ var GEOJSON_GEOMETRY_TYPES = /* @__PURE__ */ new Set([
1656
+ "Point",
1657
+ "MultiPoint",
1658
+ "LineString",
1659
+ "MultiLineString",
1660
+ "Polygon",
1661
+ "MultiPolygon",
1662
+ "GeometryCollection"
1663
+ ]);
1664
+ function _isGeoJsonNumber(v) {
1665
+ return typeof v === "number" && Number.isFinite(v);
1666
+ }
1667
+ function _isGeoJsonPosition(v) {
1668
+ return Array.isArray(v) && (v.length === 2 || v.length === 3) && v.every(_isGeoJsonNumber);
1669
+ }
1670
+ function _geoJsonPositionsEqual(a, b) {
1671
+ return a.length === b.length && a.every((v, i) => v === b[i]);
1672
+ }
1673
+ function _geoJsonRingSignedArea(ring) {
1674
+ let sum = 0;
1675
+ for (let i = 0; i < ring.length - 1; i++) {
1676
+ const [x1, y1] = ring[i];
1677
+ const [x2, y2] = ring[i + 1];
1678
+ sum += x1 * y2 - x2 * y1;
1679
+ }
1680
+ return sum / 2;
1681
+ }
1682
+ function _validateGeoJsonPolygonCoords(coords, path, errors, warnings) {
1683
+ if (!Array.isArray(coords) || coords.length === 0) {
1684
+ errors.push({ path, message: "Polygon coordinates must be a non-empty array of linear rings" });
1685
+ return;
1686
+ }
1687
+ coords.forEach((ring, i) => {
1688
+ const ringPath = `${path}[${i}]`;
1689
+ if (!Array.isArray(ring) || ring.length < 4 || !ring.every(_isGeoJsonPosition)) {
1690
+ errors.push({ path: ringPath, message: "each linear ring must have at least 4 positions" });
1691
+ return;
1692
+ }
1693
+ const first = ring[0];
1694
+ const last = ring[ring.length - 1];
1695
+ if (!_geoJsonPositionsEqual(first, last)) {
1696
+ errors.push({ path: ringPath, message: "the first and last positions of a linear ring must be identical (the ring must be closed)" });
1697
+ return;
1698
+ }
1699
+ const area = _geoJsonRingSignedArea(ring);
1700
+ const isExterior = i === 0;
1701
+ if (isExterior && area < 0) {
1702
+ warnings.push({ path: ringPath, message: "exterior ring winds clockwise; RFC 7946 recommends counterclockwise for exterior rings (right-hand rule)" });
1703
+ } else if (!isExterior && area > 0) {
1704
+ warnings.push({ path: ringPath, message: "interior ring (hole) winds counterclockwise; RFC 7946 recommends clockwise for interior rings (right-hand rule)" });
1705
+ }
1706
+ });
1707
+ }
1708
+ function _validateGeoJsonGeometry(geom, path, errors, warnings) {
1709
+ if (geom === null) return;
1710
+ if (typeof geom !== "object" || Array.isArray(geom)) {
1711
+ errors.push({ path, message: "geometry must be an object or null" });
1712
+ return;
1713
+ }
1714
+ const type = geom.type;
1715
+ if (typeof type !== "string" || !GEOJSON_GEOMETRY_TYPES.has(type)) {
1716
+ errors.push({ path: `${path}.type`, message: `geometry type must be one of ${[...GEOJSON_GEOMETRY_TYPES].join(", ")}` });
1717
+ return;
1718
+ }
1719
+ if (type === "GeometryCollection") {
1720
+ if (!Array.isArray(geom.geometries)) {
1721
+ errors.push({ path: `${path}.geometries`, message: 'GeometryCollection must have a "geometries" array' });
1722
+ return;
1723
+ }
1724
+ geom.geometries.forEach((g, i) => _validateGeoJsonGeometry(g, `${path}.geometries[${i}]`, errors, warnings));
1725
+ return;
1726
+ }
1727
+ const coords = geom.coordinates;
1728
+ const coordPath = `${path}.coordinates`;
1729
+ switch (type) {
1730
+ case "Point":
1731
+ if (!_isGeoJsonPosition(coords)) errors.push({ path: coordPath, message: "Point coordinates must be an array of 2 or 3 numbers" });
1732
+ break;
1733
+ case "MultiPoint":
1734
+ if (!Array.isArray(coords) || !coords.every(_isGeoJsonPosition)) {
1735
+ errors.push({ path: coordPath, message: "MultiPoint coordinates must be an array of positions" });
1736
+ }
1737
+ break;
1738
+ case "LineString":
1739
+ if (!Array.isArray(coords) || coords.length < 2 || !coords.every(_isGeoJsonPosition)) {
1740
+ errors.push({ path: coordPath, message: "LineString coordinates must be an array of at least 2 positions" });
1741
+ }
1742
+ break;
1743
+ case "MultiLineString":
1744
+ if (!Array.isArray(coords) || !coords.every((line) => Array.isArray(line) && line.length >= 2 && line.every(_isGeoJsonPosition))) {
1745
+ errors.push({ path: coordPath, message: "MultiLineString coordinates must be an array of LineString coordinate arrays" });
1746
+ }
1747
+ break;
1748
+ case "Polygon":
1749
+ _validateGeoJsonPolygonCoords(coords, coordPath, errors, warnings);
1750
+ break;
1751
+ case "MultiPolygon":
1752
+ if (!Array.isArray(coords)) {
1753
+ errors.push({ path: coordPath, message: "MultiPolygon coordinates must be an array of Polygon coordinate arrays" });
1754
+ } else {
1755
+ coords.forEach((poly, i) => _validateGeoJsonPolygonCoords(poly, `${coordPath}[${i}]`, errors, warnings));
1756
+ }
1757
+ break;
1758
+ }
1759
+ }
1760
+ function _validateGeoJsonBbox(bbox, path, errors) {
1761
+ if (bbox === void 0) return;
1762
+ if (!Array.isArray(bbox) || bbox.length % 2 !== 0 || bbox.length < 4 || !bbox.every(_isGeoJsonNumber)) {
1763
+ errors.push({ path: `${path}.bbox`, message: "bbox must be an array of 2*n numbers (min/max per axis)" });
1764
+ }
1765
+ }
1766
+ function _validateGeoJsonFeature(feature, path, errors, warnings) {
1767
+ if (typeof feature !== "object" || feature === null || Array.isArray(feature)) {
1768
+ errors.push({ path, message: "Feature must be an object" });
1769
+ return;
1770
+ }
1771
+ if (feature.type !== "Feature") {
1772
+ errors.push({ path: `${path}.type`, message: '"type" must be "Feature"' });
1773
+ }
1774
+ if (!("properties" in feature) || typeof feature.properties !== "object") {
1775
+ errors.push({ path: `${path}.properties`, message: '"properties" member is required and must be an object or null' });
1776
+ }
1777
+ if (!("geometry" in feature)) {
1778
+ errors.push({ path: `${path}.geometry`, message: '"geometry" member is required (an object or null)' });
1779
+ } else {
1780
+ _validateGeoJsonGeometry(feature.geometry, `${path}.geometry`, errors, warnings);
1781
+ }
1782
+ _validateGeoJsonBbox(feature.bbox, path, errors);
1783
+ }
1784
+ function validateGeoJson(input) {
1785
+ let doc;
1786
+ if (typeof input === "string") {
1787
+ try {
1788
+ doc = JSON.parse(input);
1789
+ } catch (e) {
1790
+ return { valid: false, type: null, errors: [{ path: "$", message: `invalid JSON: ${e.message}` }], warnings: [] };
1791
+ }
1792
+ } else {
1793
+ doc = input;
1794
+ }
1795
+ if (typeof doc !== "object" || doc === null || Array.isArray(doc)) {
1796
+ return { valid: false, type: null, errors: [{ path: "$", message: "document must be a JSON object" }], warnings: [] };
1797
+ }
1798
+ const type = doc.type;
1799
+ if (typeof type !== "string") {
1800
+ return { valid: false, type: null, errors: [{ path: "$.type", message: '"type" member is required' }], warnings: [] };
1801
+ }
1802
+ const errors = [];
1803
+ const warnings = [];
1804
+ _validateGeoJsonBbox(doc.bbox, "$", errors);
1805
+ if (type === "FeatureCollection") {
1806
+ if (!Array.isArray(doc.features)) {
1807
+ errors.push({ path: "$.features", message: 'FeatureCollection must have a "features" array' });
1808
+ } else {
1809
+ doc.features.forEach((feature, i) => _validateGeoJsonFeature(feature, `$.features[${i}]`, errors, warnings));
1810
+ }
1811
+ } else if (type === "Feature") {
1812
+ _validateGeoJsonFeature(doc, "$", errors, warnings);
1813
+ } else if (GEOJSON_GEOMETRY_TYPES.has(type)) {
1814
+ _validateGeoJsonGeometry(doc, "$", errors, warnings);
1815
+ } else {
1816
+ errors.push({ path: "$.type", message: '"type" must be one of Feature, FeatureCollection, or a geometry type' });
1817
+ }
1818
+ return { valid: errors.length === 0, type, errors, warnings };
1819
+ }
1654
1820
 
1655
- export { applyJsonMergePatch, applyJsonPatch, csvToJson, detectFormat, diffJson, diffJsonPatch, evaluateJsonPath, formatDiffLine, formatJson, generateExample, generateJsonPatch, getJsonStats, inferGoType, inferPythonType, inferType, jsonSchemaToTypescript, jsonToCsv, jsonToGo, jsonToPython, jsonToTypescript, jsonToYaml, jsonToZod, json_exports, mergeJsonPatch, minifyJson, parseJson, parseStructs, resolveJsonPointer, toPascalCase, toSnakeCase, validateJson, validateJsonPath, validateJsonString, yamlToJson };
1821
+ export { applyJsonMergePatch, applyJsonPatch, csvToJson, detectFormat, diffJson, diffJsonPatch, evaluateJsonPath, formatDiffLine, formatJson, generateExample, generateJsonPatch, getJsonStats, inferGoType, inferPythonType, inferType, jsonSchemaToTypescript, jsonToCsv, jsonToGo, jsonToPython, jsonToTypescript, jsonToYaml, jsonToZod, json_exports, mergeJsonPatch, minifyJson, parseJson, parseStructs, resolveJsonPointer, toPascalCase, toSnakeCase, validateGeoJson, validateJson, validateJsonPath, validateJsonString, yamlToJson };
@@ -66,6 +66,7 @@ __export(units_exports, {
66
66
  convertAprApy: () => convertAprApy,
67
67
  convertBytes: () => convertBytes,
68
68
  convertIp: () => convertIp,
69
+ convertTimecode: () => convertTimecode,
69
70
  coverBox: () => coverBox,
70
71
  decimalToIp: () => decimalToIp,
71
72
  decimalToIpCidr: () => decimalToIpCidr,
@@ -3304,5 +3305,118 @@ function calculatePositionSize(input) {
3304
3305
  targetOnCorrectSide
3305
3306
  };
3306
3307
  }
3308
+ var TIMECODE_NOMINAL_FPS = {
3309
+ "23.976": 24,
3310
+ "24": 24,
3311
+ "25": 25,
3312
+ "29.97": 30,
3313
+ "30": 30,
3314
+ "50": 50,
3315
+ "59.94": 60,
3316
+ "60": 60
3317
+ };
3318
+ function _timecodeNominalFps(frameRate) {
3319
+ return TIMECODE_NOMINAL_FPS[String(frameRate)] ?? null;
3320
+ }
3321
+ function _timecodeIsDropFrameEligible(frameRate) {
3322
+ return frameRate === 29.97 || frameRate === 59.94;
3323
+ }
3324
+ function _framesToPlainTimecode(frameNumber, nominalFps) {
3325
+ const framesPerDay = nominalFps * 86400;
3326
+ const fn = (frameNumber % framesPerDay + framesPerDay) % framesPerDay;
3327
+ const frames = fn % nominalFps;
3328
+ const totalSeconds = Math.floor(fn / nominalFps);
3329
+ return {
3330
+ hours: Math.floor(totalSeconds / 3600) % 24,
3331
+ minutes: Math.floor(totalSeconds / 60) % 60,
3332
+ seconds: totalSeconds % 60,
3333
+ frames
3334
+ };
3335
+ }
3336
+ function _plainTimecodeToFrames(hh, mm, ss, ff, nominalFps) {
3337
+ return nominalFps * 3600 * hh + nominalFps * 60 * mm + nominalFps * ss + ff;
3338
+ }
3339
+ function _framesToDropFrameTimecode(frameNumber, nominalFps, frameRate) {
3340
+ const dropFrames = nominalFps === 30 ? 2 : 4;
3341
+ const framesPer24Hours = Math.round(frameRate * 86400);
3342
+ const framesPer10Minutes = Math.round(frameRate * 600);
3343
+ const framesPerMinute = nominalFps * 60 - dropFrames;
3344
+ const fn = (frameNumber % framesPer24Hours + framesPer24Hours) % framesPer24Hours;
3345
+ const d = Math.floor(fn / framesPer10Minutes);
3346
+ const m = fn % framesPer10Minutes;
3347
+ let frameNumberOut = fn + 9 * dropFrames * d;
3348
+ if (m > dropFrames) {
3349
+ frameNumberOut += dropFrames * Math.floor((m - dropFrames) / framesPerMinute);
3350
+ }
3351
+ const frames = frameNumberOut % nominalFps;
3352
+ const totalSeconds = Math.floor(frameNumberOut / nominalFps);
3353
+ return {
3354
+ hours: Math.floor(totalSeconds / 3600) % 24,
3355
+ minutes: Math.floor(totalSeconds / 60) % 60,
3356
+ seconds: totalSeconds % 60,
3357
+ frames
3358
+ };
3359
+ }
3360
+ function _dropFrameTimecodeToFrames(hh, mm, ss, ff, nominalFps) {
3361
+ const dropFrames = nominalFps === 30 ? 2 : 4;
3362
+ const totalMinutes = 60 * hh + mm;
3363
+ return nominalFps * 3600 * hh + nominalFps * 60 * mm + nominalFps * ss + ff - dropFrames * (totalMinutes - Math.floor(totalMinutes / 10));
3364
+ }
3365
+ function _formatTimecode(parts, dropFrame) {
3366
+ const p2 = (n) => String(n).padStart(2, "0");
3367
+ const sep = dropFrame ? ";" : ":";
3368
+ return `${p2(parts.hours)}:${p2(parts.minutes)}:${p2(parts.seconds)}${sep}${p2(parts.frames)}`;
3369
+ }
3370
+ function convertTimecode(input) {
3371
+ const { mode, frameRate } = input;
3372
+ const dropFrame = input.dropFrame ?? false;
3373
+ const nominalFps = _timecodeNominalFps(frameRate);
3374
+ if (nominalFps === null) {
3375
+ return { error: "frameRate must be one of 23.976, 24, 25, 29.97, 30, 50, 59.94, 60" };
3376
+ }
3377
+ if (dropFrame && !_timecodeIsDropFrameEligible(frameRate)) {
3378
+ return { error: "dropFrame is only valid for frameRate 29.97 or 59.94" };
3379
+ }
3380
+ if (mode === "frames-to-timecode") {
3381
+ const { frameCount } = input;
3382
+ if (typeof frameCount !== "number" || !Number.isFinite(frameCount) || frameCount < 0 || Math.floor(frameCount) !== frameCount) {
3383
+ return { error: 'frameCount must be a non-negative integer for mode "frames-to-timecode"' };
3384
+ }
3385
+ const parts = dropFrame ? _framesToDropFrameTimecode(frameCount, nominalFps, frameRate) : _framesToPlainTimecode(frameCount, nominalFps);
3386
+ return {
3387
+ mode,
3388
+ frameRate,
3389
+ dropFrame,
3390
+ frameCount,
3391
+ timecode: _formatTimecode(parts, dropFrame),
3392
+ seconds: Math.round(frameCount / frameRate * 1e6) / 1e6
3393
+ };
3394
+ }
3395
+ if (mode === "timecode-to-frames") {
3396
+ const { timecode } = input;
3397
+ if (typeof timecode !== "string") return { error: 'timecode must be a string for mode "timecode-to-frames"' };
3398
+ const match = /^(\d{2}):(\d{2}):(\d{2})[:;](\d{2})$/.exec(timecode.trim());
3399
+ if (!match) return { error: "timecode must be in the form HH:MM:SS:FF (or HH:MM:SS;FF)" };
3400
+ const [, hhS, mmS, ssS, ffS] = match;
3401
+ const hh = Number(hhS), mm = Number(mmS), ss = Number(ssS), ff = Number(ffS);
3402
+ if (hh > 23) return { error: "hours must be 00-23" };
3403
+ if (mm > 59) return { error: "minutes must be 00-59" };
3404
+ if (ss > 59) return { error: "seconds must be 00-59" };
3405
+ if (ff >= nominalFps) return { error: `frames must be 00-${nominalFps - 1} at this frame rate` };
3406
+ if (dropFrame && ff <= 1 && ss === 0 && mm % 10 !== 0) {
3407
+ return { error: `frame ${ffS} at ${hhS}:${mmS}:${ssS} does not exist in drop-frame timecode: frames 00 and 01 are dropped at the start of every minute except every 10th minute` };
3408
+ }
3409
+ const frameCount = dropFrame ? _dropFrameTimecodeToFrames(hh, mm, ss, ff, nominalFps) : _plainTimecodeToFrames(hh, mm, ss, ff, nominalFps);
3410
+ return {
3411
+ mode,
3412
+ frameRate,
3413
+ dropFrame,
3414
+ frameCount,
3415
+ timecode: _formatTimecode({ hours: hh, minutes: mm, seconds: ss, frames: ff }, dropFrame),
3416
+ seconds: Math.round(frameCount / frameRate * 1e6) / 1e6
3417
+ };
3418
+ }
3419
+ return { error: 'mode must be "frames-to-timecode" or "timecode-to-frames"' };
3420
+ }
3307
3421
 
3308
- 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, calculateAnnuityPayout, calculateBlendedLoanRate, calculateBreakEven, calculateCashBreakdown, calculateCashRounding, calculateCdMaturity, calculateChange, calculateCompoundInterest, calculateCreditCardMinimumPayment, calculateCreditUtilization, calculateDebtToIncomeRatio, calculateDepreciationSchedule, calculateDiscount, calculateEmi, calculateExtraPaymentPayoff, calculateFreelancerRate, calculateGpa, calculateLoan, calculateLoanPrepaymentPenalty, calculateLoanRefinance, calculateMarginMarkup, calculateMortgagePoints, calculateMovingAverages, calculatePaybackPeriod, calculatePercentageChange, calculatePositionSize, calculateRealRateOfReturn, calculateRequiredBandwidth, calculateRoi, calculateRuleOf72, calculateSalesCommission, calculateSavingsGoal, calculateSimpleInterest, calculateTax, calculateTaxEquivalentYield, calculateTimeValue, calculateTipSplit, calculateTransferTime, calculateVideoBitrate, compareSemver, compareUnitPrices, convert, convertAprApy, convertBytes, convertIp, coverBox, decimalToIp, decimalToIpCidr, describePermissions, detectNegativeAmortization, estimateAudioFileSize, fitIntoBox, formatNumber, formatValue, formatVwValue, formatWithSeparator, gcd, intersectSemverRanges, 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, validateRoutingNumber, validateVin, viewportToPx };
3422
+ 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, calculateAnnuityPayout, calculateBlendedLoanRate, calculateBreakEven, calculateCashBreakdown, calculateCashRounding, calculateCdMaturity, calculateChange, calculateCompoundInterest, calculateCreditCardMinimumPayment, calculateCreditUtilization, calculateDebtToIncomeRatio, calculateDepreciationSchedule, calculateDiscount, calculateEmi, calculateExtraPaymentPayoff, calculateFreelancerRate, calculateGpa, calculateLoan, calculateLoanPrepaymentPenalty, calculateLoanRefinance, calculateMarginMarkup, calculateMortgagePoints, calculateMovingAverages, calculatePaybackPeriod, calculatePercentageChange, calculatePositionSize, calculateRealRateOfReturn, calculateRequiredBandwidth, calculateRoi, calculateRuleOf72, calculateSalesCommission, calculateSavingsGoal, calculateSimpleInterest, calculateTax, calculateTaxEquivalentYield, calculateTimeValue, calculateTipSplit, calculateTransferTime, calculateVideoBitrate, compareSemver, compareUnitPrices, convert, convertAprApy, convertBytes, convertIp, convertTimecode, coverBox, decimalToIp, decimalToIpCidr, describePermissions, detectNegativeAmortization, estimateAudioFileSize, fitIntoBox, formatNumber, formatValue, formatVwValue, formatWithSeparator, gcd, intersectSemverRanges, 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, validateRoutingNumber, validateVin, viewportToPx };
package/dist/index.cjs CHANGED
@@ -71,6 +71,7 @@ __export(json_exports, {
71
71
  resolveJsonPointer: () => resolveJsonPointer,
72
72
  toPascalCase: () => toPascalCase,
73
73
  toSnakeCase: () => toSnakeCase,
74
+ validateGeoJson: () => validateGeoJson,
74
75
  validateJson: () => validateJson,
75
76
  validateJsonPath: () => validateJsonPath,
76
77
  validateJsonString: () => validateJsonString,
@@ -1689,6 +1690,171 @@ function generateJsonPatch(aJson, bJson) {
1689
1690
  }
1690
1691
  return { patch: diffJsonPatch(a, b) };
1691
1692
  }
1693
+ var GEOJSON_GEOMETRY_TYPES = /* @__PURE__ */ new Set([
1694
+ "Point",
1695
+ "MultiPoint",
1696
+ "LineString",
1697
+ "MultiLineString",
1698
+ "Polygon",
1699
+ "MultiPolygon",
1700
+ "GeometryCollection"
1701
+ ]);
1702
+ function _isGeoJsonNumber(v) {
1703
+ return typeof v === "number" && Number.isFinite(v);
1704
+ }
1705
+ function _isGeoJsonPosition(v) {
1706
+ return Array.isArray(v) && (v.length === 2 || v.length === 3) && v.every(_isGeoJsonNumber);
1707
+ }
1708
+ function _geoJsonPositionsEqual(a, b) {
1709
+ return a.length === b.length && a.every((v, i) => v === b[i]);
1710
+ }
1711
+ function _geoJsonRingSignedArea(ring) {
1712
+ let sum = 0;
1713
+ for (let i = 0; i < ring.length - 1; i++) {
1714
+ const [x1, y1] = ring[i];
1715
+ const [x2, y2] = ring[i + 1];
1716
+ sum += x1 * y2 - x2 * y1;
1717
+ }
1718
+ return sum / 2;
1719
+ }
1720
+ function _validateGeoJsonPolygonCoords(coords, path, errors, warnings) {
1721
+ if (!Array.isArray(coords) || coords.length === 0) {
1722
+ errors.push({ path, message: "Polygon coordinates must be a non-empty array of linear rings" });
1723
+ return;
1724
+ }
1725
+ coords.forEach((ring, i) => {
1726
+ const ringPath = `${path}[${i}]`;
1727
+ if (!Array.isArray(ring) || ring.length < 4 || !ring.every(_isGeoJsonPosition)) {
1728
+ errors.push({ path: ringPath, message: "each linear ring must have at least 4 positions" });
1729
+ return;
1730
+ }
1731
+ const first = ring[0];
1732
+ const last = ring[ring.length - 1];
1733
+ if (!_geoJsonPositionsEqual(first, last)) {
1734
+ errors.push({ path: ringPath, message: "the first and last positions of a linear ring must be identical (the ring must be closed)" });
1735
+ return;
1736
+ }
1737
+ const area = _geoJsonRingSignedArea(ring);
1738
+ const isExterior = i === 0;
1739
+ if (isExterior && area < 0) {
1740
+ warnings.push({ path: ringPath, message: "exterior ring winds clockwise; RFC 7946 recommends counterclockwise for exterior rings (right-hand rule)" });
1741
+ } else if (!isExterior && area > 0) {
1742
+ warnings.push({ path: ringPath, message: "interior ring (hole) winds counterclockwise; RFC 7946 recommends clockwise for interior rings (right-hand rule)" });
1743
+ }
1744
+ });
1745
+ }
1746
+ function _validateGeoJsonGeometry(geom, path, errors, warnings) {
1747
+ if (geom === null) return;
1748
+ if (typeof geom !== "object" || Array.isArray(geom)) {
1749
+ errors.push({ path, message: "geometry must be an object or null" });
1750
+ return;
1751
+ }
1752
+ const type = geom.type;
1753
+ if (typeof type !== "string" || !GEOJSON_GEOMETRY_TYPES.has(type)) {
1754
+ errors.push({ path: `${path}.type`, message: `geometry type must be one of ${[...GEOJSON_GEOMETRY_TYPES].join(", ")}` });
1755
+ return;
1756
+ }
1757
+ if (type === "GeometryCollection") {
1758
+ if (!Array.isArray(geom.geometries)) {
1759
+ errors.push({ path: `${path}.geometries`, message: 'GeometryCollection must have a "geometries" array' });
1760
+ return;
1761
+ }
1762
+ geom.geometries.forEach((g, i) => _validateGeoJsonGeometry(g, `${path}.geometries[${i}]`, errors, warnings));
1763
+ return;
1764
+ }
1765
+ const coords = geom.coordinates;
1766
+ const coordPath = `${path}.coordinates`;
1767
+ switch (type) {
1768
+ case "Point":
1769
+ if (!_isGeoJsonPosition(coords)) errors.push({ path: coordPath, message: "Point coordinates must be an array of 2 or 3 numbers" });
1770
+ break;
1771
+ case "MultiPoint":
1772
+ if (!Array.isArray(coords) || !coords.every(_isGeoJsonPosition)) {
1773
+ errors.push({ path: coordPath, message: "MultiPoint coordinates must be an array of positions" });
1774
+ }
1775
+ break;
1776
+ case "LineString":
1777
+ if (!Array.isArray(coords) || coords.length < 2 || !coords.every(_isGeoJsonPosition)) {
1778
+ errors.push({ path: coordPath, message: "LineString coordinates must be an array of at least 2 positions" });
1779
+ }
1780
+ break;
1781
+ case "MultiLineString":
1782
+ if (!Array.isArray(coords) || !coords.every((line) => Array.isArray(line) && line.length >= 2 && line.every(_isGeoJsonPosition))) {
1783
+ errors.push({ path: coordPath, message: "MultiLineString coordinates must be an array of LineString coordinate arrays" });
1784
+ }
1785
+ break;
1786
+ case "Polygon":
1787
+ _validateGeoJsonPolygonCoords(coords, coordPath, errors, warnings);
1788
+ break;
1789
+ case "MultiPolygon":
1790
+ if (!Array.isArray(coords)) {
1791
+ errors.push({ path: coordPath, message: "MultiPolygon coordinates must be an array of Polygon coordinate arrays" });
1792
+ } else {
1793
+ coords.forEach((poly, i) => _validateGeoJsonPolygonCoords(poly, `${coordPath}[${i}]`, errors, warnings));
1794
+ }
1795
+ break;
1796
+ }
1797
+ }
1798
+ function _validateGeoJsonBbox(bbox, path, errors) {
1799
+ if (bbox === void 0) return;
1800
+ if (!Array.isArray(bbox) || bbox.length % 2 !== 0 || bbox.length < 4 || !bbox.every(_isGeoJsonNumber)) {
1801
+ errors.push({ path: `${path}.bbox`, message: "bbox must be an array of 2*n numbers (min/max per axis)" });
1802
+ }
1803
+ }
1804
+ function _validateGeoJsonFeature(feature, path, errors, warnings) {
1805
+ if (typeof feature !== "object" || feature === null || Array.isArray(feature)) {
1806
+ errors.push({ path, message: "Feature must be an object" });
1807
+ return;
1808
+ }
1809
+ if (feature.type !== "Feature") {
1810
+ errors.push({ path: `${path}.type`, message: '"type" must be "Feature"' });
1811
+ }
1812
+ if (!("properties" in feature) || typeof feature.properties !== "object") {
1813
+ errors.push({ path: `${path}.properties`, message: '"properties" member is required and must be an object or null' });
1814
+ }
1815
+ if (!("geometry" in feature)) {
1816
+ errors.push({ path: `${path}.geometry`, message: '"geometry" member is required (an object or null)' });
1817
+ } else {
1818
+ _validateGeoJsonGeometry(feature.geometry, `${path}.geometry`, errors, warnings);
1819
+ }
1820
+ _validateGeoJsonBbox(feature.bbox, path, errors);
1821
+ }
1822
+ function validateGeoJson(input) {
1823
+ let doc;
1824
+ if (typeof input === "string") {
1825
+ try {
1826
+ doc = JSON.parse(input);
1827
+ } catch (e) {
1828
+ return { valid: false, type: null, errors: [{ path: "$", message: `invalid JSON: ${e.message}` }], warnings: [] };
1829
+ }
1830
+ } else {
1831
+ doc = input;
1832
+ }
1833
+ if (typeof doc !== "object" || doc === null || Array.isArray(doc)) {
1834
+ return { valid: false, type: null, errors: [{ path: "$", message: "document must be a JSON object" }], warnings: [] };
1835
+ }
1836
+ const type = doc.type;
1837
+ if (typeof type !== "string") {
1838
+ return { valid: false, type: null, errors: [{ path: "$.type", message: '"type" member is required' }], warnings: [] };
1839
+ }
1840
+ const errors = [];
1841
+ const warnings = [];
1842
+ _validateGeoJsonBbox(doc.bbox, "$", errors);
1843
+ if (type === "FeatureCollection") {
1844
+ if (!Array.isArray(doc.features)) {
1845
+ errors.push({ path: "$.features", message: 'FeatureCollection must have a "features" array' });
1846
+ } else {
1847
+ doc.features.forEach((feature, i) => _validateGeoJsonFeature(feature, `$.features[${i}]`, errors, warnings));
1848
+ }
1849
+ } else if (type === "Feature") {
1850
+ _validateGeoJsonFeature(doc, "$", errors, warnings);
1851
+ } else if (GEOJSON_GEOMETRY_TYPES.has(type)) {
1852
+ _validateGeoJsonGeometry(doc, "$", errors, warnings);
1853
+ } else {
1854
+ errors.push({ path: "$.type", message: '"type" must be one of Feature, FeatureCollection, or a geometry type' });
1855
+ }
1856
+ return { valid: errors.length === 0, type, errors, warnings };
1857
+ }
1692
1858
 
1693
1859
  // src/tools/encoding.ts
1694
1860
  var encoding_exports = {};
@@ -9510,6 +9676,7 @@ __export(units_exports, {
9510
9676
  convertAprApy: () => convertAprApy,
9511
9677
  convertBytes: () => convertBytes,
9512
9678
  convertIp: () => convertIp,
9679
+ convertTimecode: () => convertTimecode,
9513
9680
  coverBox: () => coverBox,
9514
9681
  decimalToIp: () => decimalToIp,
9515
9682
  decimalToIpCidr: () => decimalToIpCidr,
@@ -12748,6 +12915,119 @@ function calculatePositionSize(input) {
12748
12915
  targetOnCorrectSide
12749
12916
  };
12750
12917
  }
12918
+ var TIMECODE_NOMINAL_FPS = {
12919
+ "23.976": 24,
12920
+ "24": 24,
12921
+ "25": 25,
12922
+ "29.97": 30,
12923
+ "30": 30,
12924
+ "50": 50,
12925
+ "59.94": 60,
12926
+ "60": 60
12927
+ };
12928
+ function _timecodeNominalFps(frameRate) {
12929
+ return TIMECODE_NOMINAL_FPS[String(frameRate)] ?? null;
12930
+ }
12931
+ function _timecodeIsDropFrameEligible(frameRate) {
12932
+ return frameRate === 29.97 || frameRate === 59.94;
12933
+ }
12934
+ function _framesToPlainTimecode(frameNumber, nominalFps) {
12935
+ const framesPerDay = nominalFps * 86400;
12936
+ const fn = (frameNumber % framesPerDay + framesPerDay) % framesPerDay;
12937
+ const frames = fn % nominalFps;
12938
+ const totalSeconds = Math.floor(fn / nominalFps);
12939
+ return {
12940
+ hours: Math.floor(totalSeconds / 3600) % 24,
12941
+ minutes: Math.floor(totalSeconds / 60) % 60,
12942
+ seconds: totalSeconds % 60,
12943
+ frames
12944
+ };
12945
+ }
12946
+ function _plainTimecodeToFrames(hh, mm, ss, ff, nominalFps) {
12947
+ return nominalFps * 3600 * hh + nominalFps * 60 * mm + nominalFps * ss + ff;
12948
+ }
12949
+ function _framesToDropFrameTimecode(frameNumber, nominalFps, frameRate) {
12950
+ const dropFrames = nominalFps === 30 ? 2 : 4;
12951
+ const framesPer24Hours = Math.round(frameRate * 86400);
12952
+ const framesPer10Minutes = Math.round(frameRate * 600);
12953
+ const framesPerMinute = nominalFps * 60 - dropFrames;
12954
+ const fn = (frameNumber % framesPer24Hours + framesPer24Hours) % framesPer24Hours;
12955
+ const d = Math.floor(fn / framesPer10Minutes);
12956
+ const m = fn % framesPer10Minutes;
12957
+ let frameNumberOut = fn + 9 * dropFrames * d;
12958
+ if (m > dropFrames) {
12959
+ frameNumberOut += dropFrames * Math.floor((m - dropFrames) / framesPerMinute);
12960
+ }
12961
+ const frames = frameNumberOut % nominalFps;
12962
+ const totalSeconds = Math.floor(frameNumberOut / nominalFps);
12963
+ return {
12964
+ hours: Math.floor(totalSeconds / 3600) % 24,
12965
+ minutes: Math.floor(totalSeconds / 60) % 60,
12966
+ seconds: totalSeconds % 60,
12967
+ frames
12968
+ };
12969
+ }
12970
+ function _dropFrameTimecodeToFrames(hh, mm, ss, ff, nominalFps) {
12971
+ const dropFrames = nominalFps === 30 ? 2 : 4;
12972
+ const totalMinutes = 60 * hh + mm;
12973
+ return nominalFps * 3600 * hh + nominalFps * 60 * mm + nominalFps * ss + ff - dropFrames * (totalMinutes - Math.floor(totalMinutes / 10));
12974
+ }
12975
+ function _formatTimecode(parts, dropFrame) {
12976
+ const p2 = (n) => String(n).padStart(2, "0");
12977
+ const sep = dropFrame ? ";" : ":";
12978
+ return `${p2(parts.hours)}:${p2(parts.minutes)}:${p2(parts.seconds)}${sep}${p2(parts.frames)}`;
12979
+ }
12980
+ function convertTimecode(input) {
12981
+ const { mode, frameRate } = input;
12982
+ const dropFrame = input.dropFrame ?? false;
12983
+ const nominalFps = _timecodeNominalFps(frameRate);
12984
+ if (nominalFps === null) {
12985
+ return { error: "frameRate must be one of 23.976, 24, 25, 29.97, 30, 50, 59.94, 60" };
12986
+ }
12987
+ if (dropFrame && !_timecodeIsDropFrameEligible(frameRate)) {
12988
+ return { error: "dropFrame is only valid for frameRate 29.97 or 59.94" };
12989
+ }
12990
+ if (mode === "frames-to-timecode") {
12991
+ const { frameCount } = input;
12992
+ if (typeof frameCount !== "number" || !Number.isFinite(frameCount) || frameCount < 0 || Math.floor(frameCount) !== frameCount) {
12993
+ return { error: 'frameCount must be a non-negative integer for mode "frames-to-timecode"' };
12994
+ }
12995
+ const parts = dropFrame ? _framesToDropFrameTimecode(frameCount, nominalFps, frameRate) : _framesToPlainTimecode(frameCount, nominalFps);
12996
+ return {
12997
+ mode,
12998
+ frameRate,
12999
+ dropFrame,
13000
+ frameCount,
13001
+ timecode: _formatTimecode(parts, dropFrame),
13002
+ seconds: Math.round(frameCount / frameRate * 1e6) / 1e6
13003
+ };
13004
+ }
13005
+ if (mode === "timecode-to-frames") {
13006
+ const { timecode } = input;
13007
+ if (typeof timecode !== "string") return { error: 'timecode must be a string for mode "timecode-to-frames"' };
13008
+ const match = /^(\d{2}):(\d{2}):(\d{2})[:;](\d{2})$/.exec(timecode.trim());
13009
+ if (!match) return { error: "timecode must be in the form HH:MM:SS:FF (or HH:MM:SS;FF)" };
13010
+ const [, hhS, mmS, ssS, ffS] = match;
13011
+ const hh = Number(hhS), mm = Number(mmS), ss = Number(ssS), ff = Number(ffS);
13012
+ if (hh > 23) return { error: "hours must be 00-23" };
13013
+ if (mm > 59) return { error: "minutes must be 00-59" };
13014
+ if (ss > 59) return { error: "seconds must be 00-59" };
13015
+ if (ff >= nominalFps) return { error: `frames must be 00-${nominalFps - 1} at this frame rate` };
13016
+ if (dropFrame && ff <= 1 && ss === 0 && mm % 10 !== 0) {
13017
+ return { error: `frame ${ffS} at ${hhS}:${mmS}:${ssS} does not exist in drop-frame timecode: frames 00 and 01 are dropped at the start of every minute except every 10th minute` };
13018
+ }
13019
+ const frameCount = dropFrame ? _dropFrameTimecodeToFrames(hh, mm, ss, ff, nominalFps) : _plainTimecodeToFrames(hh, mm, ss, ff, nominalFps);
13020
+ return {
13021
+ mode,
13022
+ frameRate,
13023
+ dropFrame,
13024
+ frameCount,
13025
+ timecode: _formatTimecode({ hours: hh, minutes: mm, seconds: ss, frames: ff }, dropFrame),
13026
+ seconds: Math.round(frameCount / frameRate * 1e6) / 1e6
13027
+ };
13028
+ }
13029
+ return { error: 'mode must be "frames-to-timecode" or "timecode-to-frames"' };
13030
+ }
12751
13031
 
12752
13032
  // src/tools/network.ts
12753
13033
  var network_exports = {};