@utilix-tech/sdk 0.20.0 → 0.22.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.22.0
6
+ - Add `convertAprApy` (apr-apy-converter): convert between a nominal APR and effective APY for a given compounding frequency, in both directions.
7
+ - Add `parseIso8601Duration` / `formatIso8601Duration` (iso8601-duration): parse an ISO 8601 duration into components and total seconds, or format seconds into a canonical duration string.
8
+
9
+ ## 0.21.0
10
+ - Add `calculatePercentageChange` (percentage-change-calculator): percentage change between two numbers, with absolute change, direction, and multiplier.
11
+ - Add `resolveJsonPointer` (json-pointer-resolver): resolve an RFC 6901 JSON Pointer against a document, with ~0/~1 escaping and clear not-found reasons.
12
+
5
13
  ## 0.20.0
6
14
  - Add `buildCacheControl` (cache-control-builder): assemble and explain a `Cache-Control` response header from directives, with warnings for combinations that cancel out.
7
15
  - Add `detectFillerWords` (filler-word-detector): scan text for filler words, hedge phrases, and clichés with per-phrase counts and a density score.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @utilix-tech/sdk
2
2
 
3
- **556 developer utility functions for Node.js: runs locally, no API key required.**
3
+ **558 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)
@@ -66,7 +66,7 @@ const { formatJson } = require("@utilix-tech/sdk/json");
66
66
 
67
67
  ```ts
68
68
  import { formatJson, minifyJson, diffJson, jsonToTypescript, validateJson,
69
- evaluateJsonPath, jsonToCsv, yamlToJson, jsonToYaml, getJsonStats } from "@utilix-tech/sdk/json";
69
+ evaluateJsonPath, resolveJsonPointer, jsonToCsv, yamlToJson, jsonToYaml, getJsonStats } from "@utilix-tech/sdk/json";
70
70
 
71
71
  // Format with 2-space indent
72
72
  formatJson('{"name":"alice","age":30}', { indent: 2 });
@@ -80,6 +80,10 @@ jsonToTypescript('{"id":1,"name":"Alice"}', { rootName: "User" });
80
80
  // Query with JSONPath
81
81
  evaluateJsonPath(obj, "$.users[*].name");
82
82
 
83
+ // Resolve an RFC 6901 JSON Pointer
84
+ resolveJsonPointer('{"users":[{"name":"Alice"}]}', "/users/0/name");
85
+ // { found: true, value: "Alice", tokens: ["users", "0", "name"] }
86
+
83
87
  // Convert JSON to CSV
84
88
  jsonToCsv('[{"a":1,"b":2}]');
85
89
 
@@ -230,7 +234,8 @@ generateData([
230
234
 
231
235
  ```ts
232
236
  import { diffDates, parseCron, convertTime, formatRelative,
233
- getNextRuns, humanizeDiff } from "@utilix-tech/sdk/time";
237
+ getNextRuns, humanizeDiff, parseIso8601Duration,
238
+ formatIso8601Duration } from "@utilix-tech/sdk/time";
234
239
 
235
240
  formatRelative(new Date("2024-01-01")); // "2 years ago"
236
241
 
@@ -245,6 +250,10 @@ parseCron("0 9 * * MON-FRI");
245
250
  // { description: "At 09:00, Monday through Friday", ... }
246
251
 
247
252
  getNextRuns("*/5 * * * *", 5); // next 5 run timestamps
253
+
254
+ // ISO 8601 durations, both directions
255
+ parseIso8601Duration("P3DT4H30M"); // { totalSeconds: 275400, days: 3, ... }
256
+ formatIso8601Duration(90061); // { iso: "P1DT1H1M1S", ... }
248
257
  ```
249
258
 
250
259
  ---
@@ -252,7 +261,7 @@ getNextRuns("*/5 * * * *", 5); // next 5 run timestamps
252
261
  ### `/units`: Conversions
253
262
 
254
263
  ```ts
255
- import { convertBytes, pxToAll, convert, formatValue, validateCardNumber, validateIban, calculateLoan, validateBarcode, calculateTax, calculateCompoundInterest, validateIsbn, calculateTipSplit, calculateSimpleInterest, calculateTimeValue } from "@utilix-tech/sdk/units";
264
+ import { convertBytes, pxToAll, convert, formatValue, validateCardNumber, validateIban, calculateLoan, validateBarcode, calculateTax, calculateCompoundInterest, validateIsbn, calculateTipSplit, calculateSimpleInterest, calculateTimeValue, calculatePercentageChange, convertAprApy } from "@utilix-tech/sdk/units";
256
265
 
257
266
  // File sizes
258
267
  convertBytes(1073741824, "GB"); // 1
@@ -310,6 +319,14 @@ calculateSimpleInterest(1000, 5, 2);
310
319
  // Present / future value (time value of money), either direction
311
320
  calculateTimeValue(1000, 5, 10, "future");
312
321
  // { presentValue: 1000, futureValue: 1628.89, growthFactor: 1.628895, ... }
322
+
323
+ // Percentage change between two numbers
324
+ calculatePercentageChange(200, 250);
325
+ // { percentChange: 25, absoluteChange: 50, direction: "increase", multiplier: 1.25 }
326
+
327
+ // APR <-> APY conversion for a given compounding frequency
328
+ convertAprApy(12, 12, "apr-to-apy");
329
+ // { apr: 12, apy: 12.682503, difference: 0.682503, compoundsPerYear: 12, ... }
313
330
  ```
314
331
 
315
332
  ---
@@ -9,6 +9,7 @@ __export(time_exports, {
9
9
  convertTime: () => convertTime,
10
10
  diffDates: () => diffDates,
11
11
  formatInTimezone: () => formatInTimezone,
12
+ formatIso8601Duration: () => formatIso8601Duration,
12
13
  formatRelative: () => formatRelative,
13
14
  formatRunDate: () => formatRunDate,
14
15
  fromIsoString: () => fromIsoString,
@@ -21,7 +22,8 @@ __export(time_exports, {
21
22
  humanizeDiff: () => humanizeDiff,
22
23
  parseCron: () => parseCron,
23
24
  parseCronExpression: () => parseCronExpression,
24
- parseDate: () => parseDate
25
+ parseDate: () => parseDate,
26
+ parseIso8601Duration: () => parseIso8601Duration
25
27
  });
26
28
  function formatRelative(diffMs) {
27
29
  const absMs = Math.abs(diffMs);
@@ -681,5 +683,96 @@ function getRunInterval(dates) {
681
683
  const days = Math.round(diffMins / 1440);
682
684
  return `Every ${days} days`;
683
685
  }
686
+ var DURATION_SECONDS_PER_YEAR = 365 * 86400;
687
+ var DURATION_SECONDS_PER_MONTH = 30 * 86400;
688
+ function humanizeDurationParts(parts) {
689
+ const pieces = [];
690
+ for (const [value, label] of parts) {
691
+ if (value === 0) continue;
692
+ pieces.push(`${value} ${label}${value === 1 ? "" : "s"}`);
693
+ }
694
+ return pieces.length > 0 ? pieces.join(", ") : "0 seconds";
695
+ }
696
+ function parseIso8601Duration(input) {
697
+ if (typeof input !== "string") return { error: "Duration must be a string" };
698
+ const trimmed = input.trim();
699
+ if (trimmed.length === 0) return { error: "Duration is empty" };
700
+ const match = trimmed.match(
701
+ /^P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)W)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+(?:\.\d+)?)S)?)?$/
702
+ );
703
+ if (!match) {
704
+ return { error: 'Not a valid ISO 8601 duration (expected a form like "P3DT4H30M")' };
705
+ }
706
+ const years = match[1] ? parseInt(match[1], 10) : 0;
707
+ const months = match[2] ? parseInt(match[2], 10) : 0;
708
+ const weeks = match[3] ? parseInt(match[3], 10) : 0;
709
+ const days = match[4] ? parseInt(match[4], 10) : 0;
710
+ const hours = match[5] ? parseInt(match[5], 10) : 0;
711
+ const minutes = match[6] ? parseInt(match[6], 10) : 0;
712
+ const seconds = match[7] ? parseFloat(match[7]) : 0;
713
+ if (years + months + weeks + days + hours + minutes + seconds === 0) {
714
+ return { error: 'Duration has no components (a bare "P" or "PT" is not valid)' };
715
+ }
716
+ const approximate = years > 0 || months > 0;
717
+ const totalSeconds = years * DURATION_SECONDS_PER_YEAR + months * DURATION_SECONDS_PER_MONTH + weeks * 7 * 86400 + days * 86400 + hours * 3600 + minutes * 60 + seconds;
718
+ const human = humanizeDurationParts([
719
+ [years, "year"],
720
+ [months, "month"],
721
+ [weeks, "week"],
722
+ [days, "day"],
723
+ [hours, "hour"],
724
+ [minutes, "minute"],
725
+ [seconds, "second"]
726
+ ]);
727
+ return {
728
+ input: trimmed,
729
+ years,
730
+ months,
731
+ weeks,
732
+ days,
733
+ hours,
734
+ minutes,
735
+ seconds,
736
+ totalSeconds: Math.round(totalSeconds * 1e6) / 1e6,
737
+ approximate,
738
+ human
739
+ };
740
+ }
741
+ function formatIso8601Duration(seconds) {
742
+ if (typeof seconds !== "number" || !Number.isFinite(seconds) || seconds < 0) {
743
+ return { error: "Seconds must be zero or a positive number" };
744
+ }
745
+ const totalDays = Math.floor(seconds / 86400);
746
+ let rem = seconds - totalDays * 86400;
747
+ const hours = Math.floor(rem / 3600);
748
+ rem -= hours * 3600;
749
+ const minutes = Math.floor(rem / 60);
750
+ let secs = rem - minutes * 60;
751
+ secs = Math.round(secs * 1e6) / 1e6;
752
+ let iso = "P";
753
+ if (totalDays > 0) iso += `${totalDays}D`;
754
+ const hasTime = hours > 0 || minutes > 0 || secs > 0;
755
+ if (hasTime) {
756
+ iso += "T";
757
+ if (hours > 0) iso += `${hours}H`;
758
+ if (minutes > 0) iso += `${minutes}M`;
759
+ if (secs > 0) iso += `${secs}S`;
760
+ }
761
+ if (iso === "P") iso = "PT0S";
762
+ const human = humanizeDurationParts([
763
+ [totalDays, "day"],
764
+ [hours, "hour"],
765
+ [minutes, "minute"],
766
+ [secs, "second"]
767
+ ]);
768
+ return {
769
+ seconds,
770
+ iso,
771
+ days: totalDays,
772
+ hours,
773
+ minutes,
774
+ human
775
+ };
776
+ }
684
777
 
685
- export { COMMON_CRONS, COMMON_DATE_PRESETS, TIMEZONES, convertTime, diffDates, formatInTimezone, formatRelative, formatRunDate, fromIsoString, fromUnix, getAllZonesForTime, getNextRuns, getNow, getRunInterval, getTimezoneOffset, humanizeDiff, parseCron, parseCronExpression, parseDate, time_exports };
778
+ export { COMMON_CRONS, COMMON_DATE_PRESETS, TIMEZONES, convertTime, diffDates, formatInTimezone, formatIso8601Duration, formatRelative, formatRunDate, fromIsoString, fromUnix, getAllZonesForTime, getNextRuns, getNow, getRunInterval, getTimezoneOffset, humanizeDiff, parseCron, parseCronExpression, parseDate, parseIso8601Duration, time_exports };
@@ -20,12 +20,14 @@ __export(units_exports, {
20
20
  calcSubnet: () => calcSubnet,
21
21
  calculateCompoundInterest: () => calculateCompoundInterest,
22
22
  calculateLoan: () => calculateLoan,
23
+ calculatePercentageChange: () => calculatePercentageChange,
23
24
  calculateSimpleInterest: () => calculateSimpleInterest,
24
25
  calculateTax: () => calculateTax,
25
26
  calculateTimeValue: () => calculateTimeValue,
26
27
  calculateTipSplit: () => calculateTipSplit,
27
28
  compareSemver: () => compareSemver,
28
29
  convert: () => convert,
30
+ convertAprApy: () => convertAprApy,
29
31
  convertBytes: () => convertBytes,
30
32
  convertIp: () => convertIp,
31
33
  coverBox: () => coverBox,
@@ -1337,5 +1339,60 @@ function calculateTimeValue(amount, annualRatePercent, years, direction, compoun
1337
1339
  interest: round22(futureValue - presentValue)
1338
1340
  };
1339
1341
  }
1342
+ function calculatePercentageChange(oldValue, newValue) {
1343
+ if (!Number.isFinite(oldValue)) return { error: "Original value must be a finite number" };
1344
+ if (!Number.isFinite(newValue)) return { error: "New value must be a finite number" };
1345
+ if (oldValue === 0 && newValue !== 0) {
1346
+ return { error: "Percentage change from zero is undefined (division by zero); use a nonzero original value" };
1347
+ }
1348
+ const round4 = (n) => Math.round(n * 1e4) / 1e4;
1349
+ const absoluteChange = newValue - oldValue;
1350
+ const percentChange = oldValue === 0 ? 0 : absoluteChange / Math.abs(oldValue) * 100;
1351
+ const multiplier = oldValue === 0 ? 1 : newValue / oldValue;
1352
+ let direction;
1353
+ if (absoluteChange > 0) direction = "increase";
1354
+ else if (absoluteChange < 0) direction = "decrease";
1355
+ else direction = "no change";
1356
+ return {
1357
+ oldValue,
1358
+ newValue,
1359
+ absoluteChange: round4(absoluteChange),
1360
+ percentChange: round4(percentChange),
1361
+ direction,
1362
+ multiplier: round4(multiplier)
1363
+ };
1364
+ }
1365
+ function convertAprApy(rate, compoundsPerYear, direction) {
1366
+ if (!Number.isFinite(rate) || rate < 0) return { error: "Rate must be zero or a positive number" };
1367
+ if (!Number.isInteger(compoundsPerYear) || compoundsPerYear <= 0) {
1368
+ return { error: "Compounding frequency must be a positive whole number of periods per year" };
1369
+ }
1370
+ if (direction !== "apr-to-apy" && direction !== "apy-to-apr") {
1371
+ return { error: 'Direction must be "apr-to-apy" or "apy-to-apr"' };
1372
+ }
1373
+ const round6 = (n2) => Math.round(n2 * 1e6) / 1e6;
1374
+ const round8 = (n2) => Math.round(n2 * 1e8) / 1e8;
1375
+ const n = compoundsPerYear;
1376
+ let aprDecimal;
1377
+ let apyDecimal;
1378
+ if (direction === "apr-to-apy") {
1379
+ aprDecimal = rate / 100;
1380
+ apyDecimal = Math.pow(1 + aprDecimal / n, n) - 1;
1381
+ } else {
1382
+ apyDecimal = rate / 100;
1383
+ aprDecimal = n * (Math.pow(1 + apyDecimal, 1 / n) - 1);
1384
+ }
1385
+ const apr = round6(aprDecimal * 100);
1386
+ const apy = round6(apyDecimal * 100);
1387
+ return {
1388
+ direction,
1389
+ compoundsPerYear: n,
1390
+ apr,
1391
+ apy,
1392
+ aprDecimal: round8(aprDecimal),
1393
+ apyDecimal: round8(apyDecimal),
1394
+ difference: round6(apy - apr)
1395
+ };
1396
+ }
1340
1397
 
1341
- 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, autoFormat, bumpVersion, calcAspectRatio, calcSubnet, calculateCompoundInterest, calculateLoan, calculateSimpleInterest, calculateTax, calculateTimeValue, calculateTipSplit, compareSemver, convert, convertBytes, convertIp, coverBox, decimalToIp, decimalToIpCidr, describePermissions, fitIntoBox, formatNumber, formatValue, formatVwValue, formatWithSeparator, gcd, ipFromBinary, ipFromDecimal, ipFromHex, ipToBinary, ipToDecimal, ipToDecimalCidr, ipToHex, ipToOctal, isValid, isValidCidr, isValidIp, isValidIpv4, octalToPermissions, parseFileSize, parseInput, parseNumberString, parseSemver, permissionsToChmod, permissionsToOctal, permissionsToSymbolic, ptToPx, pxToAll, pxToViewport, remToPx, satisfiesRange, scaleByHeight, scaleByWidth, sortVersions, splitSubnet, symbolicToPermissions, units_exports, validateBarcode, validateCardNumber, validateIban, validateIsbn, viewportToPx };
1398
+ 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, autoFormat, bumpVersion, calcAspectRatio, calcSubnet, calculateCompoundInterest, calculateLoan, calculatePercentageChange, calculateSimpleInterest, calculateTax, calculateTimeValue, calculateTipSplit, compareSemver, 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, octalToPermissions, parseFileSize, parseInput, parseNumberString, parseSemver, permissionsToChmod, permissionsToOctal, permissionsToSymbolic, ptToPx, pxToAll, pxToViewport, remToPx, satisfiesRange, scaleByHeight, scaleByWidth, sortVersions, splitSubnet, symbolicToPermissions, units_exports, validateBarcode, validateCardNumber, validateIban, validateIsbn, viewportToPx };
@@ -24,6 +24,7 @@ __export(json_exports, {
24
24
  minifyJson: () => minifyJson,
25
25
  parseJson: () => parseJson,
26
26
  parseStructs: () => parseStructs,
27
+ resolveJsonPointer: () => resolveJsonPointer,
27
28
  toPascalCase: () => toPascalCase,
28
29
  toSnakeCase: () => toSnakeCase,
29
30
  validateJson: () => validateJson,
@@ -1249,5 +1250,57 @@ function jsonToZod(json, options) {
1249
1250
 
1250
1251
  export const ${rootName}Schema = ${schema}`;
1251
1252
  }
1253
+ function _unescapePointerToken(token) {
1254
+ return token.replace(/~1/g, "/").replace(/~0/g, "~");
1255
+ }
1256
+ function resolveJsonPointer(json, pointer) {
1257
+ let parsed;
1258
+ try {
1259
+ parsed = JSON.parse(json);
1260
+ } catch {
1261
+ return { pointer, found: false, tokens: [], error: "Invalid JSON input" };
1262
+ }
1263
+ let ptr = pointer;
1264
+ if (ptr.startsWith("#")) {
1265
+ ptr = ptr.slice(1);
1266
+ try {
1267
+ ptr = decodeURIComponent(ptr);
1268
+ } catch {
1269
+ return { pointer, found: false, tokens: [], error: "Invalid percent-encoding in URI-fragment pointer" };
1270
+ }
1271
+ }
1272
+ if (ptr === "") {
1273
+ return { pointer, found: true, value: parsed, tokens: [] };
1274
+ }
1275
+ if (!ptr.startsWith("/")) {
1276
+ return { pointer, found: false, tokens: [], error: "Pointer must be empty or start with '/' (RFC 6901)" };
1277
+ }
1278
+ const tokens = ptr.split("/").slice(1).map(_unescapePointerToken);
1279
+ let current = parsed;
1280
+ for (const token of tokens) {
1281
+ if (Array.isArray(current)) {
1282
+ if (token === "-") {
1283
+ return { pointer, found: false, tokens, failedAt: token, reason: '"-" refers to the nonexistent element after the end of the array' };
1284
+ }
1285
+ if (!/^(0|[1-9][0-9]*)$/.test(token)) {
1286
+ return { pointer, found: false, tokens, failedAt: token, reason: `"${token}" is not a valid array index` };
1287
+ }
1288
+ const idx = Number(token);
1289
+ if (idx >= current.length) {
1290
+ return { pointer, found: false, tokens, failedAt: token, reason: `array index ${idx} is out of bounds (length ${current.length})` };
1291
+ }
1292
+ current = current[idx];
1293
+ } else if (current !== null && typeof current === "object") {
1294
+ const obj = current;
1295
+ if (!Object.prototype.hasOwnProperty.call(obj, token)) {
1296
+ return { pointer, found: false, tokens, failedAt: token, reason: `key "${token}" not found` };
1297
+ }
1298
+ current = obj[token];
1299
+ } else {
1300
+ return { pointer, found: false, tokens, failedAt: token, reason: `cannot descend into ${current === null ? "null" : typeof current} at "${token}"` };
1301
+ }
1302
+ }
1303
+ return { pointer, found: true, value: current, tokens };
1304
+ }
1252
1305
 
1253
- export { csvToJson, detectFormat, diffJson, evaluateJsonPath, formatDiffLine, formatJson, generateExample, getJsonStats, inferGoType, inferPythonType, inferType, jsonToCsv, jsonToGo, jsonToPython, jsonToTypescript, jsonToYaml, jsonToZod, json_exports, minifyJson, parseJson, parseStructs, toPascalCase, toSnakeCase, validateJson, validateJsonPath, validateJsonString, yamlToJson };
1306
+ export { csvToJson, detectFormat, diffJson, evaluateJsonPath, formatDiffLine, formatJson, generateExample, getJsonStats, inferGoType, inferPythonType, inferType, jsonToCsv, jsonToGo, jsonToPython, jsonToTypescript, jsonToYaml, jsonToZod, json_exports, minifyJson, parseJson, parseStructs, resolveJsonPointer, toPascalCase, toSnakeCase, validateJson, validateJsonPath, validateJsonString, yamlToJson };
package/dist/index.cjs CHANGED
@@ -62,6 +62,7 @@ __export(json_exports, {
62
62
  minifyJson: () => minifyJson,
63
63
  parseJson: () => parseJson,
64
64
  parseStructs: () => parseStructs,
65
+ resolveJsonPointer: () => resolveJsonPointer,
65
66
  toPascalCase: () => toPascalCase,
66
67
  toSnakeCase: () => toSnakeCase,
67
68
  validateJson: () => validateJson,
@@ -1287,6 +1288,58 @@ function jsonToZod(json, options) {
1287
1288
 
1288
1289
  export const ${rootName}Schema = ${schema}`;
1289
1290
  }
1291
+ function _unescapePointerToken(token) {
1292
+ return token.replace(/~1/g, "/").replace(/~0/g, "~");
1293
+ }
1294
+ function resolveJsonPointer(json, pointer) {
1295
+ let parsed;
1296
+ try {
1297
+ parsed = JSON.parse(json);
1298
+ } catch {
1299
+ return { pointer, found: false, tokens: [], error: "Invalid JSON input" };
1300
+ }
1301
+ let ptr = pointer;
1302
+ if (ptr.startsWith("#")) {
1303
+ ptr = ptr.slice(1);
1304
+ try {
1305
+ ptr = decodeURIComponent(ptr);
1306
+ } catch {
1307
+ return { pointer, found: false, tokens: [], error: "Invalid percent-encoding in URI-fragment pointer" };
1308
+ }
1309
+ }
1310
+ if (ptr === "") {
1311
+ return { pointer, found: true, value: parsed, tokens: [] };
1312
+ }
1313
+ if (!ptr.startsWith("/")) {
1314
+ return { pointer, found: false, tokens: [], error: "Pointer must be empty or start with '/' (RFC 6901)" };
1315
+ }
1316
+ const tokens = ptr.split("/").slice(1).map(_unescapePointerToken);
1317
+ let current = parsed;
1318
+ for (const token of tokens) {
1319
+ if (Array.isArray(current)) {
1320
+ if (token === "-") {
1321
+ return { pointer, found: false, tokens, failedAt: token, reason: '"-" refers to the nonexistent element after the end of the array' };
1322
+ }
1323
+ if (!/^(0|[1-9][0-9]*)$/.test(token)) {
1324
+ return { pointer, found: false, tokens, failedAt: token, reason: `"${token}" is not a valid array index` };
1325
+ }
1326
+ const idx = Number(token);
1327
+ if (idx >= current.length) {
1328
+ return { pointer, found: false, tokens, failedAt: token, reason: `array index ${idx} is out of bounds (length ${current.length})` };
1329
+ }
1330
+ current = current[idx];
1331
+ } else if (current !== null && typeof current === "object") {
1332
+ const obj = current;
1333
+ if (!Object.prototype.hasOwnProperty.call(obj, token)) {
1334
+ return { pointer, found: false, tokens, failedAt: token, reason: `key "${token}" not found` };
1335
+ }
1336
+ current = obj[token];
1337
+ } else {
1338
+ return { pointer, found: false, tokens, failedAt: token, reason: `cannot descend into ${current === null ? "null" : typeof current} at "${token}"` };
1339
+ }
1340
+ }
1341
+ return { pointer, found: true, value: current, tokens };
1342
+ }
1290
1343
 
1291
1344
  // src/tools/encoding.ts
1292
1345
  var encoding_exports = {};
@@ -6091,6 +6144,7 @@ __export(time_exports, {
6091
6144
  convertTime: () => convertTime,
6092
6145
  diffDates: () => diffDates,
6093
6146
  formatInTimezone: () => formatInTimezone,
6147
+ formatIso8601Duration: () => formatIso8601Duration,
6094
6148
  formatRelative: () => formatRelative,
6095
6149
  formatRunDate: () => formatRunDate,
6096
6150
  fromIsoString: () => fromIsoString,
@@ -6103,7 +6157,8 @@ __export(time_exports, {
6103
6157
  humanizeDiff: () => humanizeDiff,
6104
6158
  parseCron: () => parseCron,
6105
6159
  parseCronExpression: () => parseCronExpression,
6106
- parseDate: () => parseDate
6160
+ parseDate: () => parseDate,
6161
+ parseIso8601Duration: () => parseIso8601Duration
6107
6162
  });
6108
6163
  function formatRelative(diffMs) {
6109
6164
  const absMs = Math.abs(diffMs);
@@ -6763,6 +6818,97 @@ function getRunInterval(dates) {
6763
6818
  const days = Math.round(diffMins / 1440);
6764
6819
  return `Every ${days} days`;
6765
6820
  }
6821
+ var DURATION_SECONDS_PER_YEAR = 365 * 86400;
6822
+ var DURATION_SECONDS_PER_MONTH = 30 * 86400;
6823
+ function humanizeDurationParts(parts) {
6824
+ const pieces = [];
6825
+ for (const [value, label] of parts) {
6826
+ if (value === 0) continue;
6827
+ pieces.push(`${value} ${label}${value === 1 ? "" : "s"}`);
6828
+ }
6829
+ return pieces.length > 0 ? pieces.join(", ") : "0 seconds";
6830
+ }
6831
+ function parseIso8601Duration(input) {
6832
+ if (typeof input !== "string") return { error: "Duration must be a string" };
6833
+ const trimmed = input.trim();
6834
+ if (trimmed.length === 0) return { error: "Duration is empty" };
6835
+ const match = trimmed.match(
6836
+ /^P(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)W)?(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+(?:\.\d+)?)S)?)?$/
6837
+ );
6838
+ if (!match) {
6839
+ return { error: 'Not a valid ISO 8601 duration (expected a form like "P3DT4H30M")' };
6840
+ }
6841
+ const years = match[1] ? parseInt(match[1], 10) : 0;
6842
+ const months = match[2] ? parseInt(match[2], 10) : 0;
6843
+ const weeks = match[3] ? parseInt(match[3], 10) : 0;
6844
+ const days = match[4] ? parseInt(match[4], 10) : 0;
6845
+ const hours = match[5] ? parseInt(match[5], 10) : 0;
6846
+ const minutes = match[6] ? parseInt(match[6], 10) : 0;
6847
+ const seconds = match[7] ? parseFloat(match[7]) : 0;
6848
+ if (years + months + weeks + days + hours + minutes + seconds === 0) {
6849
+ return { error: 'Duration has no components (a bare "P" or "PT" is not valid)' };
6850
+ }
6851
+ const approximate = years > 0 || months > 0;
6852
+ const totalSeconds = years * DURATION_SECONDS_PER_YEAR + months * DURATION_SECONDS_PER_MONTH + weeks * 7 * 86400 + days * 86400 + hours * 3600 + minutes * 60 + seconds;
6853
+ const human = humanizeDurationParts([
6854
+ [years, "year"],
6855
+ [months, "month"],
6856
+ [weeks, "week"],
6857
+ [days, "day"],
6858
+ [hours, "hour"],
6859
+ [minutes, "minute"],
6860
+ [seconds, "second"]
6861
+ ]);
6862
+ return {
6863
+ input: trimmed,
6864
+ years,
6865
+ months,
6866
+ weeks,
6867
+ days,
6868
+ hours,
6869
+ minutes,
6870
+ seconds,
6871
+ totalSeconds: Math.round(totalSeconds * 1e6) / 1e6,
6872
+ approximate,
6873
+ human
6874
+ };
6875
+ }
6876
+ function formatIso8601Duration(seconds) {
6877
+ if (typeof seconds !== "number" || !Number.isFinite(seconds) || seconds < 0) {
6878
+ return { error: "Seconds must be zero or a positive number" };
6879
+ }
6880
+ const totalDays = Math.floor(seconds / 86400);
6881
+ let rem = seconds - totalDays * 86400;
6882
+ const hours = Math.floor(rem / 3600);
6883
+ rem -= hours * 3600;
6884
+ const minutes = Math.floor(rem / 60);
6885
+ let secs = rem - minutes * 60;
6886
+ secs = Math.round(secs * 1e6) / 1e6;
6887
+ let iso = "P";
6888
+ if (totalDays > 0) iso += `${totalDays}D`;
6889
+ const hasTime = hours > 0 || minutes > 0 || secs > 0;
6890
+ if (hasTime) {
6891
+ iso += "T";
6892
+ if (hours > 0) iso += `${hours}H`;
6893
+ if (minutes > 0) iso += `${minutes}M`;
6894
+ if (secs > 0) iso += `${secs}S`;
6895
+ }
6896
+ if (iso === "P") iso = "PT0S";
6897
+ const human = humanizeDurationParts([
6898
+ [totalDays, "day"],
6899
+ [hours, "hour"],
6900
+ [minutes, "minute"],
6901
+ [secs, "second"]
6902
+ ]);
6903
+ return {
6904
+ seconds,
6905
+ iso,
6906
+ days: totalDays,
6907
+ hours,
6908
+ minutes,
6909
+ human
6910
+ };
6911
+ }
6766
6912
 
6767
6913
  // src/tools/units.ts
6768
6914
  var units_exports = {};
@@ -6784,12 +6930,14 @@ __export(units_exports, {
6784
6930
  calcSubnet: () => calcSubnet,
6785
6931
  calculateCompoundInterest: () => calculateCompoundInterest,
6786
6932
  calculateLoan: () => calculateLoan,
6933
+ calculatePercentageChange: () => calculatePercentageChange,
6787
6934
  calculateSimpleInterest: () => calculateSimpleInterest,
6788
6935
  calculateTax: () => calculateTax,
6789
6936
  calculateTimeValue: () => calculateTimeValue,
6790
6937
  calculateTipSplit: () => calculateTipSplit,
6791
6938
  compareSemver: () => compareSemver,
6792
6939
  convert: () => convert,
6940
+ convertAprApy: () => convertAprApy,
6793
6941
  convertBytes: () => convertBytes,
6794
6942
  convertIp: () => convertIp,
6795
6943
  coverBox: () => coverBox,
@@ -8101,6 +8249,61 @@ function calculateTimeValue(amount, annualRatePercent, years, direction, compoun
8101
8249
  interest: round23(futureValue - presentValue)
8102
8250
  };
8103
8251
  }
8252
+ function calculatePercentageChange(oldValue, newValue) {
8253
+ if (!Number.isFinite(oldValue)) return { error: "Original value must be a finite number" };
8254
+ if (!Number.isFinite(newValue)) return { error: "New value must be a finite number" };
8255
+ if (oldValue === 0 && newValue !== 0) {
8256
+ return { error: "Percentage change from zero is undefined (division by zero); use a nonzero original value" };
8257
+ }
8258
+ const round4 = (n) => Math.round(n * 1e4) / 1e4;
8259
+ const absoluteChange = newValue - oldValue;
8260
+ const percentChange = oldValue === 0 ? 0 : absoluteChange / Math.abs(oldValue) * 100;
8261
+ const multiplier = oldValue === 0 ? 1 : newValue / oldValue;
8262
+ let direction;
8263
+ if (absoluteChange > 0) direction = "increase";
8264
+ else if (absoluteChange < 0) direction = "decrease";
8265
+ else direction = "no change";
8266
+ return {
8267
+ oldValue,
8268
+ newValue,
8269
+ absoluteChange: round4(absoluteChange),
8270
+ percentChange: round4(percentChange),
8271
+ direction,
8272
+ multiplier: round4(multiplier)
8273
+ };
8274
+ }
8275
+ function convertAprApy(rate, compoundsPerYear, direction) {
8276
+ if (!Number.isFinite(rate) || rate < 0) return { error: "Rate must be zero or a positive number" };
8277
+ if (!Number.isInteger(compoundsPerYear) || compoundsPerYear <= 0) {
8278
+ return { error: "Compounding frequency must be a positive whole number of periods per year" };
8279
+ }
8280
+ if (direction !== "apr-to-apy" && direction !== "apy-to-apr") {
8281
+ return { error: 'Direction must be "apr-to-apy" or "apy-to-apr"' };
8282
+ }
8283
+ const round6 = (n2) => Math.round(n2 * 1e6) / 1e6;
8284
+ const round8 = (n2) => Math.round(n2 * 1e8) / 1e8;
8285
+ const n = compoundsPerYear;
8286
+ let aprDecimal;
8287
+ let apyDecimal;
8288
+ if (direction === "apr-to-apy") {
8289
+ aprDecimal = rate / 100;
8290
+ apyDecimal = Math.pow(1 + aprDecimal / n, n) - 1;
8291
+ } else {
8292
+ apyDecimal = rate / 100;
8293
+ aprDecimal = n * (Math.pow(1 + apyDecimal, 1 / n) - 1);
8294
+ }
8295
+ const apr = round6(aprDecimal * 100);
8296
+ const apy = round6(apyDecimal * 100);
8297
+ return {
8298
+ direction,
8299
+ compoundsPerYear: n,
8300
+ apr,
8301
+ apy,
8302
+ aprDecimal: round8(aprDecimal),
8303
+ apyDecimal: round8(apyDecimal),
8304
+ difference: round6(apy - apr)
8305
+ };
8306
+ }
8104
8307
 
8105
8308
  // src/tools/network.ts
8106
8309
  var network_exports = {};
package/dist/index.d.cts CHANGED
@@ -1,11 +1,11 @@
1
- export { j as json } from './json-BjSoIS1h.cjs';
1
+ export { j as json } from './json-DEVFIpsC.cjs';
2
2
  export { e as encoding } from './encoding-Dk3wpmb3.cjs';
3
3
  export { h as hashing } from './hashing-CnetQFD_.cjs';
4
4
  export { t as text } from './text-DS70aMY5.cjs';
5
5
  export { d as data } from './data-D8XTI7Du.cjs';
6
6
  export { g as generators } from './generators-BGtRGpJZ.cjs';
7
- export { t as time } from './time-DbT8fjaF.cjs';
8
- export { u as units } from './units-BFTQwpBu.cjs';
7
+ export { t as time } from './time-KqJjdqII.cjs';
8
+ export { u as units } from './units-KrjRyu0w.cjs';
9
9
  export { n as network } from './network-BJOjJnmY.cjs';
10
10
  export { a as api } from './api-Xl7OzIgq.cjs';
11
11
  export { c as code } from './code-DaRvff2v.cjs';
package/dist/index.d.ts CHANGED
@@ -1,11 +1,11 @@
1
- export { j as json } from './json-BjSoIS1h.js';
1
+ export { j as json } from './json-DEVFIpsC.js';
2
2
  export { e as encoding } from './encoding-Dk3wpmb3.js';
3
3
  export { h as hashing } from './hashing-CnetQFD_.js';
4
4
  export { t as text } from './text-DS70aMY5.js';
5
5
  export { d as data } from './data-D8XTI7Du.js';
6
6
  export { g as generators } from './generators-BGtRGpJZ.js';
7
- export { t as time } from './time-DbT8fjaF.js';
8
- export { u as units } from './units-BFTQwpBu.js';
7
+ export { t as time } from './time-KqJjdqII.js';
8
+ export { u as units } from './units-KrjRyu0w.js';
9
9
  export { n as network } from './network-BJOjJnmY.js';
10
10
  export { a as api } from './api-Xl7OzIgq.js';
11
11
  export { c as code } from './code-DaRvff2v.js';
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  export { media_exports as media } from './chunk-XMKJYVKB.js';
2
- export { units_exports as units } from './chunk-U34B5XPK.js';
2
+ export { units_exports as units } from './chunk-TLRSQHMP.js';
3
3
  export { network_exports as network } from './chunk-Y5OOP6WN.js';
4
4
  export { api_exports as api } from './chunk-Q3B3YWOA.js';
5
5
  export { code_exports as code } from './chunk-LXKIPPDG.js';
@@ -7,13 +7,13 @@ export { color_exports as color } from './chunk-BPVAB4P2.js';
7
7
  export { css_exports as css } from './chunk-6YPV2AB5.js';
8
8
  export { misc_exports as misc } from './chunk-GX7H6TAX.js';
9
9
  export { ai_agent_exports as ai_agent } from './chunk-265NPGKH.js';
10
- export { json_exports as json } from './chunk-TSAGO3XP.js';
10
+ export { json_exports as json } from './chunk-WO3LJMVV.js';
11
11
  export { encoding_exports as encoding } from './chunk-DOWQZUTP.js';
12
12
  export { hashing_exports as hashing } from './chunk-GEFVMGZR.js';
13
13
  export { text_exports as text } from './chunk-2I4O5YCT.js';
14
14
  export { data_exports as data } from './chunk-TINQFTLO.js';
15
15
  export { generators_exports as generators } from './chunk-2CJSLYWI.js';
16
- export { time_exports as time } from './chunk-ZPQZEIXP.js';
16
+ export { time_exports as time } from './chunk-IEPWZERQ.js';
17
17
  import './chunk-MLKGABMK.js';
18
18
 
19
19
  // src/index.ts