@utilix-tech/sdk 0.96.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,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.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
+
9
+ ## 0.97.0
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.
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".
12
+
5
13
  ## 0.96.0
6
14
  - Add `validateRoutingNumber` (aba-routing-number-validator): validate a 9-digit US bank routing number via its weighted mod-10 checksum, identifying the routing number's type and Federal Reserve district from its first two digits.
7
15
  - Add `auditMarkdownImageAltText` (markdown-image-alt-text-auditor): scan a Markdown document's inline images and flag alt text accessibility smells, missing/empty alt text, generic placeholder words, a redundant "image of" prefix, and alt text that's just the image's own filename.
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: [...] }
@@ -792,6 +800,16 @@ validateRoutingNumber("021000021");
792
800
  import { auditMarkdownImageAltText } from "@utilix-tech/sdk/data";
793
801
  auditMarkdownImageAltText("![](photo.png)\n![Photo of a mountain](mountain.jpg)");
794
802
  // { images: [...], issues: [{ line: 1, type: "empty-alt", ... }, { line: 2, type: "redundant-prefix", ... }], imageCount: 2 }
803
+
804
+ // Compute a trade's position size and risk from account size, entry, and stop-loss
805
+ import { calculatePositionSize } from "@utilix-tech/sdk/units";
806
+ calculatePositionSize({ accountSize: 10000, riskPercent: 1, entryPrice: 50, stopLossPrice: 48, targetPrice: 56 });
807
+ // { direction: "long", riskAmount: 100, perUnitRisk: 2, positionSizeUnits: 50, positionValue: 2500, dollarRisk: 100, riskRewardRatio: 3, targetOnCorrectSide: true }
808
+
809
+ // Scan text for weasel words that imply evidence without naming a source
810
+ import { detectWeaselWords } from "@utilix-tech/sdk/text";
811
+ detectWeaselWords("Studies show that many experts believe this works.");
812
+ // { totalWords: 8, totalMatches: 2, density: 25, matches: [{ phrase: "many experts believe", count: 1, indices: [...] }, { phrase: "studies show", count: 1, indices: [...] }] }
795
813
  ```
796
814
 
797
815
  ---
@@ -29,6 +29,7 @@ __export(text_exports, {
29
29
  detectDelimiter: () => detectDelimiter,
30
30
  detectFillerWords: () => detectFillerWords,
31
31
  detectPassiveVoice: () => detectPassiveVoice,
32
+ detectWeaselWords: () => detectWeaselWords,
32
33
  detectWordyPhrases: () => detectWordyPhrases,
33
34
  diffLines: () => diffLines,
34
35
  diffLinesSimple: () => diffLinesSimple,
@@ -3330,5 +3331,61 @@ function checkAcronymExpansion(text) {
3330
3331
  issues.sort((a, b) => a.index - b.index);
3331
3332
  return { acronyms, issues };
3332
3333
  }
3334
+ var WEASEL_PHRASES = [
3335
+ "many experts believe",
3336
+ "some experts say",
3337
+ "experts agree",
3338
+ "critics argue",
3339
+ "studies show",
3340
+ "studies have shown",
3341
+ "research shows",
3342
+ "research suggests",
3343
+ "it is widely believed",
3344
+ "it is widely known",
3345
+ "it is widely regarded",
3346
+ "it is generally accepted",
3347
+ "it is often said",
3348
+ "it has been suggested",
3349
+ "it is said that",
3350
+ "some say",
3351
+ "some people believe",
3352
+ "some people think",
3353
+ "many believe",
3354
+ "many people think",
3355
+ "many claim",
3356
+ "observers note",
3357
+ "sources say",
3358
+ "according to some",
3359
+ "widely regarded as",
3360
+ "many consider",
3361
+ "some scientists believe",
3362
+ "some argue",
3363
+ "reportedly",
3364
+ "allegedly"
3365
+ ];
3366
+ function _weaselRegex(phrase) {
3367
+ const tokens = phrase.split(/\s+/).map((t) => t.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
3368
+ return new RegExp("\\b" + tokens.join("\\s+") + "\\b", "gi");
3369
+ }
3370
+ function detectWeaselWords(text) {
3371
+ const totalWords = (text.match(/[A-Za-z0-9']+/g) ?? []).length;
3372
+ const matches = [];
3373
+ for (const phrase of WEASEL_PHRASES) {
3374
+ const re = _weaselRegex(phrase);
3375
+ const indices = [];
3376
+ let m;
3377
+ while ((m = re.exec(text)) !== null) {
3378
+ indices.push(m.index);
3379
+ if (m.index === re.lastIndex) re.lastIndex++;
3380
+ }
3381
+ if (indices.length > 0) {
3382
+ matches.push({ phrase, count: indices.length, indices });
3383
+ }
3384
+ }
3385
+ matches.sort((a, b) => b.count - a.count || a.phrase.localeCompare(b.phrase));
3386
+ const totalMatches = matches.reduce((s, x) => s + x.count, 0);
3387
+ const density = totalWords > 0 ? Math.round(totalMatches / totalWords * 1e3) / 10 : 0;
3388
+ return { totalWords, totalMatches, density, matches };
3389
+ }
3333
3390
 
3334
- export { CASE_FORMATS, DEFAULT_CONVERT_OPTIONS, EMOJI_TABLE, ESCAPE_MODES, LINE_OPS, MORSE_CODE, addBorder, analyzeKeywordDensity, analyzeSentenceLengths, analyzeText, applyOp, applyOps, calculateStringDistance, centerText, checkAcronymExpansion, checkDashUsage, checkOxfordCommaConsistency, convertAll, convertCase, countWords, csvToHtml, csvToMarkdown, detectDelimiter, detectFillerWords, detectPassiveVoice, detectWordyPhrases, diffLines, diffLinesSimple, diffStats, escapeAll, escapeString, extractHeadings, formatReadingTime, generate, generateDiff, generateHtmlTable, generateMarkdownTable, generateParagraphs, generateSentences, generateWords, getAvailableFonts, getEmojiByCategory, getEmojiByChar, getEmojiByShortcode, getEmojiCategories, getMorseForChar, getMorseTiming, htmlToMarkdown, isValidMorse, markdownToHtml, morseToText, numberToOrdinal, numberToRoman, numberToWords, ordinalWords, parseDiff, parseTableData, readabilityScore, renderMarkdown, romanToNumber, scoreReadability, searchEmoji, sentenceCount, slugify, slugifyAll, syllableCount, textToAscii, textToMorse, text_exports, unescapeString, wordFrequency };
3391
+ export { CASE_FORMATS, DEFAULT_CONVERT_OPTIONS, EMOJI_TABLE, ESCAPE_MODES, LINE_OPS, MORSE_CODE, addBorder, analyzeKeywordDensity, analyzeSentenceLengths, analyzeText, applyOp, applyOps, calculateStringDistance, centerText, checkAcronymExpansion, checkDashUsage, checkOxfordCommaConsistency, convertAll, convertCase, countWords, csvToHtml, csvToMarkdown, detectDelimiter, detectFillerWords, detectPassiveVoice, detectWeaselWords, detectWordyPhrases, diffLines, diffLinesSimple, diffStats, escapeAll, escapeString, extractHeadings, formatReadingTime, generate, generateDiff, generateHtmlTable, generateMarkdownTable, generateParagraphs, generateSentences, generateWords, getAvailableFonts, getEmojiByCategory, getEmojiByChar, getEmojiByShortcode, getEmojiCategories, getMorseForChar, getMorseTiming, htmlToMarkdown, isValidMorse, markdownToHtml, morseToText, numberToOrdinal, numberToRoman, numberToWords, ordinalWords, parseDiff, parseTableData, readabilityScore, renderMarkdown, romanToNumber, scoreReadability, searchEmoji, sentenceCount, slugify, slugifyAll, syllableCount, textToAscii, textToMorse, text_exports, unescapeString, wordFrequency };
@@ -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 };
@@ -46,6 +46,7 @@ __export(units_exports, {
46
46
  calculateMovingAverages: () => calculateMovingAverages,
47
47
  calculatePaybackPeriod: () => calculatePaybackPeriod,
48
48
  calculatePercentageChange: () => calculatePercentageChange,
49
+ calculatePositionSize: () => calculatePositionSize,
49
50
  calculateRealRateOfReturn: () => calculateRealRateOfReturn,
50
51
  calculateRequiredBandwidth: () => calculateRequiredBandwidth,
51
52
  calculateRoi: () => calculateRoi,
@@ -65,6 +66,7 @@ __export(units_exports, {
65
66
  convertAprApy: () => convertAprApy,
66
67
  convertBytes: () => convertBytes,
67
68
  convertIp: () => convertIp,
69
+ convertTimecode: () => convertTimecode,
68
70
  coverBox: () => coverBox,
69
71
  decimalToIp: () => decimalToIp,
70
72
  decimalToIpCidr: () => decimalToIpCidr,
@@ -3271,5 +3273,150 @@ function validateRoutingNumber(input) {
3271
3273
  federalReserveDistrict: district
3272
3274
  };
3273
3275
  }
3276
+ function calculatePositionSize(input) {
3277
+ const { accountSize, riskPercent, entryPrice, stopLossPrice, targetPrice } = input;
3278
+ if (!(accountSize > 0)) return { error: "accountSize must be greater than 0" };
3279
+ if (!(riskPercent > 0) || riskPercent > 100) return { error: "riskPercent must be greater than 0 and at most 100" };
3280
+ if (!(entryPrice > 0)) return { error: "entryPrice must be greater than 0" };
3281
+ if (!(stopLossPrice > 0)) return { error: "stopLossPrice must be greater than 0" };
3282
+ if (entryPrice === stopLossPrice) return { error: "entryPrice and stopLossPrice must differ" };
3283
+ if (targetPrice !== void 0 && !(targetPrice > 0)) return { error: "targetPrice must be greater than 0" };
3284
+ const direction = stopLossPrice < entryPrice ? "long" : "short";
3285
+ const riskAmount = accountSize * (riskPercent / 100);
3286
+ const perUnitRisk = Math.abs(entryPrice - stopLossPrice);
3287
+ const positionSizeUnits = Math.floor(riskAmount / perUnitRisk);
3288
+ const positionValue = positionSizeUnits * entryPrice;
3289
+ const dollarRisk = positionSizeUnits * perUnitRisk;
3290
+ let riskRewardRatio = null;
3291
+ let targetOnCorrectSide = null;
3292
+ if (targetPrice !== void 0) {
3293
+ const reward = Math.abs(targetPrice - entryPrice);
3294
+ riskRewardRatio = Math.round(reward / perUnitRisk * 100) / 100;
3295
+ targetOnCorrectSide = direction === "long" ? targetPrice > entryPrice : targetPrice < entryPrice;
3296
+ }
3297
+ return {
3298
+ direction,
3299
+ riskAmount: Math.round(riskAmount * 100) / 100,
3300
+ perUnitRisk: Math.round(perUnitRisk * 1e4) / 1e4,
3301
+ positionSizeUnits,
3302
+ positionValue: Math.round(positionValue * 100) / 100,
3303
+ dollarRisk: Math.round(dollarRisk * 100) / 100,
3304
+ riskRewardRatio,
3305
+ targetOnCorrectSide
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
+ }
3274
3421
 
3275
- 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, 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 };