@utilix-tech/sdk 0.21.0 → 0.23.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.23.0
6
+ - Add `calculateMarginMarkup` (margin-markup-calculator): given exactly two of cost, price, margin percent, and markup percent, solve for the other two plus the gross profit.
7
+ - Add `marginToMarkup` / `markupToMargin` helpers for converting between the two percentage bases.
8
+
9
+ ## 0.22.0
10
+ - Add `convertAprApy` (apr-apy-converter): convert between a nominal APR and effective APY for a given compounding frequency, in both directions.
11
+ - Add `parseIso8601Duration` / `formatIso8601Duration` (iso8601-duration): parse an ISO 8601 duration into components and total seconds, or format seconds into a canonical duration string.
12
+
5
13
  ## 0.21.0
6
14
  - Add `calculatePercentageChange` (percentage-change-calculator): percentage change between two numbers, with absolute change, direction, and multiplier.
7
15
  - Add `resolveJsonPointer` (json-pointer-resolver): resolve an RFC 6901 JSON Pointer against a document, with ~0/~1 escaping and clear not-found reasons.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @utilix-tech/sdk
2
2
 
3
- **558 developer utility functions for Node.js: runs locally, no API key required.**
3
+ **564 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)
@@ -234,7 +234,8 @@ generateData([
234
234
 
235
235
  ```ts
236
236
  import { diffDates, parseCron, convertTime, formatRelative,
237
- getNextRuns, humanizeDiff } from "@utilix-tech/sdk/time";
237
+ getNextRuns, humanizeDiff, parseIso8601Duration,
238
+ formatIso8601Duration } from "@utilix-tech/sdk/time";
238
239
 
239
240
  formatRelative(new Date("2024-01-01")); // "2 years ago"
240
241
 
@@ -249,6 +250,10 @@ parseCron("0 9 * * MON-FRI");
249
250
  // { description: "At 09:00, Monday through Friday", ... }
250
251
 
251
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", ... }
252
257
  ```
253
258
 
254
259
  ---
@@ -256,7 +261,7 @@ getNextRuns("*/5 * * * *", 5); // next 5 run timestamps
256
261
  ### `/units`: Conversions
257
262
 
258
263
  ```ts
259
- import { convertBytes, pxToAll, convert, formatValue, validateCardNumber, validateIban, calculateLoan, validateBarcode, calculateTax, calculateCompoundInterest, validateIsbn, calculateTipSplit, calculateSimpleInterest, calculateTimeValue, calculatePercentageChange } from "@utilix-tech/sdk/units";
264
+ import { convertBytes, pxToAll, convert, formatValue, validateCardNumber, validateIban, calculateLoan, validateBarcode, calculateTax, calculateCompoundInterest, validateIsbn, calculateTipSplit, calculateSimpleInterest, calculateTimeValue, calculatePercentageChange, convertAprApy, calculateMarginMarkup } from "@utilix-tech/sdk/units";
260
265
 
261
266
  // File sizes
262
267
  convertBytes(1073741824, "GB"); // 1
@@ -318,6 +323,14 @@ calculateTimeValue(1000, 5, 10, "future");
318
323
  // Percentage change between two numbers
319
324
  calculatePercentageChange(200, 250);
320
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, ... }
330
+
331
+ // Margin/markup: pass any TWO of cost, price, marginPercent, markupPercent
332
+ calculateMarginMarkup({ cost: 80, price: 100 });
333
+ // { cost: 80, price: 100, profit: 20, marginPercent: 20, markupPercent: 25, ... }
321
334
  ```
322
335
 
323
336
  ---
@@ -20,6 +20,7 @@ __export(units_exports, {
20
20
  calcSubnet: () => calcSubnet,
21
21
  calculateCompoundInterest: () => calculateCompoundInterest,
22
22
  calculateLoan: () => calculateLoan,
23
+ calculateMarginMarkup: () => calculateMarginMarkup,
23
24
  calculatePercentageChange: () => calculatePercentageChange,
24
25
  calculateSimpleInterest: () => calculateSimpleInterest,
25
26
  calculateTax: () => calculateTax,
@@ -27,6 +28,7 @@ __export(units_exports, {
27
28
  calculateTipSplit: () => calculateTipSplit,
28
29
  compareSemver: () => compareSemver,
29
30
  convert: () => convert,
31
+ convertAprApy: () => convertAprApy,
30
32
  convertBytes: () => convertBytes,
31
33
  convertIp: () => convertIp,
32
34
  coverBox: () => coverBox,
@@ -51,6 +53,8 @@ __export(units_exports, {
51
53
  isValidCidr: () => isValidCidr,
52
54
  isValidIp: () => isValidIp,
53
55
  isValidIpv4: () => isValidIpv4,
56
+ marginToMarkup: () => marginToMarkup,
57
+ markupToMargin: () => markupToMargin,
54
58
  octalToPermissions: () => octalToPermissions,
55
59
  parseFileSize: () => parseFileSize,
56
60
  parseInput: () => parseInput,
@@ -1361,5 +1365,104 @@ function calculatePercentageChange(oldValue, newValue) {
1361
1365
  multiplier: round4(multiplier)
1362
1366
  };
1363
1367
  }
1368
+ function convertAprApy(rate, compoundsPerYear, direction) {
1369
+ if (!Number.isFinite(rate) || rate < 0) return { error: "Rate must be zero or a positive number" };
1370
+ if (!Number.isInteger(compoundsPerYear) || compoundsPerYear <= 0) {
1371
+ return { error: "Compounding frequency must be a positive whole number of periods per year" };
1372
+ }
1373
+ if (direction !== "apr-to-apy" && direction !== "apy-to-apr") {
1374
+ return { error: 'Direction must be "apr-to-apy" or "apy-to-apr"' };
1375
+ }
1376
+ const round6 = (n2) => Math.round(n2 * 1e6) / 1e6;
1377
+ const round8 = (n2) => Math.round(n2 * 1e8) / 1e8;
1378
+ const n = compoundsPerYear;
1379
+ let aprDecimal;
1380
+ let apyDecimal;
1381
+ if (direction === "apr-to-apy") {
1382
+ aprDecimal = rate / 100;
1383
+ apyDecimal = Math.pow(1 + aprDecimal / n, n) - 1;
1384
+ } else {
1385
+ apyDecimal = rate / 100;
1386
+ aprDecimal = n * (Math.pow(1 + apyDecimal, 1 / n) - 1);
1387
+ }
1388
+ const apr = round6(aprDecimal * 100);
1389
+ const apy = round6(apyDecimal * 100);
1390
+ return {
1391
+ direction,
1392
+ compoundsPerYear: n,
1393
+ apr,
1394
+ apy,
1395
+ aprDecimal: round8(aprDecimal),
1396
+ apyDecimal: round8(apyDecimal),
1397
+ difference: round6(apy - apr)
1398
+ };
1399
+ }
1400
+ var _MARGIN_FIELDS = ["cost", "price", "marginPercent", "markupPercent"];
1401
+ function calculateMarginMarkup(input) {
1402
+ const provided = (v) => typeof v === "number" && Number.isFinite(v);
1403
+ const given = _MARGIN_FIELDS.filter((f) => provided(input[f]));
1404
+ if (given.length !== 2) {
1405
+ return { error: `Provide exactly two of cost, price, marginPercent, markupPercent (got ${given.length})` };
1406
+ }
1407
+ const has = (f) => given.includes(f);
1408
+ const cost0 = input.cost;
1409
+ const price0 = input.price;
1410
+ const margin0 = input.marginPercent;
1411
+ const markup0 = input.markupPercent;
1412
+ if (has("cost") && cost0 <= 0) return { error: "Cost must be greater than zero" };
1413
+ if (has("price") && price0 <= 0) return { error: "Price must be greater than zero" };
1414
+ if (has("marginPercent") && margin0 >= 100) {
1415
+ return { error: "Margin percent must be below 100 (a 100% margin implies a zero cost)" };
1416
+ }
1417
+ if (has("markupPercent") && markup0 <= -100) {
1418
+ return { error: "Markup percent must be greater than -100" };
1419
+ }
1420
+ let cost;
1421
+ let price;
1422
+ if (has("cost") && has("price")) {
1423
+ cost = cost0;
1424
+ price = price0;
1425
+ } else if (has("cost") && has("marginPercent")) {
1426
+ cost = cost0;
1427
+ price = cost0 / (1 - margin0 / 100);
1428
+ } else if (has("cost") && has("markupPercent")) {
1429
+ cost = cost0;
1430
+ price = cost0 * (1 + markup0 / 100);
1431
+ } else if (has("price") && has("marginPercent")) {
1432
+ price = price0;
1433
+ cost = price0 * (1 - margin0 / 100);
1434
+ } else if (has("price") && has("markupPercent")) {
1435
+ price = price0;
1436
+ cost = price0 / (1 + markup0 / 100);
1437
+ } else {
1438
+ return { error: "Margin and markup alone cannot be solved: also provide a cost or a price" };
1439
+ }
1440
+ if (!Number.isFinite(cost) || !Number.isFinite(price)) return { error: "Inputs produce a non-finite result" };
1441
+ if (cost <= 0) return { error: "Those inputs imply a cost of zero or less" };
1442
+ if (price <= 0) return { error: "Those inputs imply a price of zero or less" };
1443
+ const round4 = (n) => Math.round(n * 1e4) / 1e4;
1444
+ const profit = price - cost;
1445
+ return {
1446
+ cost: round4(cost),
1447
+ price: round4(price),
1448
+ profit: round4(profit),
1449
+ marginPercent: round4(profit / price * 100),
1450
+ markupPercent: round4(profit / cost * 100),
1451
+ given,
1452
+ solvedFor: _MARGIN_FIELDS.filter((f) => !given.includes(f))
1453
+ };
1454
+ }
1455
+ function marginToMarkup(marginPercent) {
1456
+ if (!Number.isFinite(marginPercent) || marginPercent >= 100) {
1457
+ return { error: "Margin percent must be a finite number below 100" };
1458
+ }
1459
+ return Math.round(marginPercent / (100 - marginPercent) * 100 * 1e4) / 1e4;
1460
+ }
1461
+ function markupToMargin(markupPercent) {
1462
+ if (!Number.isFinite(markupPercent) || markupPercent <= -100) {
1463
+ return { error: "Markup percent must be a finite number greater than -100" };
1464
+ }
1465
+ return Math.round(markupPercent / (100 + markupPercent) * 100 * 1e4) / 1e4;
1466
+ }
1364
1467
 
1365
- 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, 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 };
1468
+ 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, calculateMarginMarkup, 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, marginToMarkup, markupToMargin, octalToPermissions, parseFileSize, parseInput, parseNumberString, parseSemver, permissionsToChmod, permissionsToOctal, permissionsToSymbolic, ptToPx, pxToAll, pxToViewport, remToPx, satisfiesRange, scaleByHeight, scaleByWidth, sortVersions, splitSubnet, symbolicToPermissions, units_exports, validateBarcode, validateCardNumber, validateIban, validateIsbn, viewportToPx };
@@ -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 };
package/dist/index.cjs CHANGED
@@ -6144,6 +6144,7 @@ __export(time_exports, {
6144
6144
  convertTime: () => convertTime,
6145
6145
  diffDates: () => diffDates,
6146
6146
  formatInTimezone: () => formatInTimezone,
6147
+ formatIso8601Duration: () => formatIso8601Duration,
6147
6148
  formatRelative: () => formatRelative,
6148
6149
  formatRunDate: () => formatRunDate,
6149
6150
  fromIsoString: () => fromIsoString,
@@ -6156,7 +6157,8 @@ __export(time_exports, {
6156
6157
  humanizeDiff: () => humanizeDiff,
6157
6158
  parseCron: () => parseCron,
6158
6159
  parseCronExpression: () => parseCronExpression,
6159
- parseDate: () => parseDate
6160
+ parseDate: () => parseDate,
6161
+ parseIso8601Duration: () => parseIso8601Duration
6160
6162
  });
6161
6163
  function formatRelative(diffMs) {
6162
6164
  const absMs = Math.abs(diffMs);
@@ -6816,6 +6818,97 @@ function getRunInterval(dates) {
6816
6818
  const days = Math.round(diffMins / 1440);
6817
6819
  return `Every ${days} days`;
6818
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
+ }
6819
6912
 
6820
6913
  // src/tools/units.ts
6821
6914
  var units_exports = {};
@@ -6837,6 +6930,7 @@ __export(units_exports, {
6837
6930
  calcSubnet: () => calcSubnet,
6838
6931
  calculateCompoundInterest: () => calculateCompoundInterest,
6839
6932
  calculateLoan: () => calculateLoan,
6933
+ calculateMarginMarkup: () => calculateMarginMarkup,
6840
6934
  calculatePercentageChange: () => calculatePercentageChange,
6841
6935
  calculateSimpleInterest: () => calculateSimpleInterest,
6842
6936
  calculateTax: () => calculateTax,
@@ -6844,6 +6938,7 @@ __export(units_exports, {
6844
6938
  calculateTipSplit: () => calculateTipSplit,
6845
6939
  compareSemver: () => compareSemver,
6846
6940
  convert: () => convert,
6941
+ convertAprApy: () => convertAprApy,
6847
6942
  convertBytes: () => convertBytes,
6848
6943
  convertIp: () => convertIp,
6849
6944
  coverBox: () => coverBox,
@@ -6868,6 +6963,8 @@ __export(units_exports, {
6868
6963
  isValidCidr: () => isValidCidr,
6869
6964
  isValidIp: () => isValidIp,
6870
6965
  isValidIpv4: () => isValidIpv4,
6966
+ marginToMarkup: () => marginToMarkup,
6967
+ markupToMargin: () => markupToMargin,
6871
6968
  octalToPermissions: () => octalToPermissions,
6872
6969
  parseFileSize: () => parseFileSize,
6873
6970
  parseInput: () => parseInput,
@@ -8178,6 +8275,105 @@ function calculatePercentageChange(oldValue, newValue) {
8178
8275
  multiplier: round4(multiplier)
8179
8276
  };
8180
8277
  }
8278
+ function convertAprApy(rate, compoundsPerYear, direction) {
8279
+ if (!Number.isFinite(rate) || rate < 0) return { error: "Rate must be zero or a positive number" };
8280
+ if (!Number.isInteger(compoundsPerYear) || compoundsPerYear <= 0) {
8281
+ return { error: "Compounding frequency must be a positive whole number of periods per year" };
8282
+ }
8283
+ if (direction !== "apr-to-apy" && direction !== "apy-to-apr") {
8284
+ return { error: 'Direction must be "apr-to-apy" or "apy-to-apr"' };
8285
+ }
8286
+ const round6 = (n2) => Math.round(n2 * 1e6) / 1e6;
8287
+ const round8 = (n2) => Math.round(n2 * 1e8) / 1e8;
8288
+ const n = compoundsPerYear;
8289
+ let aprDecimal;
8290
+ let apyDecimal;
8291
+ if (direction === "apr-to-apy") {
8292
+ aprDecimal = rate / 100;
8293
+ apyDecimal = Math.pow(1 + aprDecimal / n, n) - 1;
8294
+ } else {
8295
+ apyDecimal = rate / 100;
8296
+ aprDecimal = n * (Math.pow(1 + apyDecimal, 1 / n) - 1);
8297
+ }
8298
+ const apr = round6(aprDecimal * 100);
8299
+ const apy = round6(apyDecimal * 100);
8300
+ return {
8301
+ direction,
8302
+ compoundsPerYear: n,
8303
+ apr,
8304
+ apy,
8305
+ aprDecimal: round8(aprDecimal),
8306
+ apyDecimal: round8(apyDecimal),
8307
+ difference: round6(apy - apr)
8308
+ };
8309
+ }
8310
+ var _MARGIN_FIELDS = ["cost", "price", "marginPercent", "markupPercent"];
8311
+ function calculateMarginMarkup(input) {
8312
+ const provided = (v) => typeof v === "number" && Number.isFinite(v);
8313
+ const given = _MARGIN_FIELDS.filter((f) => provided(input[f]));
8314
+ if (given.length !== 2) {
8315
+ return { error: `Provide exactly two of cost, price, marginPercent, markupPercent (got ${given.length})` };
8316
+ }
8317
+ const has = (f) => given.includes(f);
8318
+ const cost0 = input.cost;
8319
+ const price0 = input.price;
8320
+ const margin0 = input.marginPercent;
8321
+ const markup0 = input.markupPercent;
8322
+ if (has("cost") && cost0 <= 0) return { error: "Cost must be greater than zero" };
8323
+ if (has("price") && price0 <= 0) return { error: "Price must be greater than zero" };
8324
+ if (has("marginPercent") && margin0 >= 100) {
8325
+ return { error: "Margin percent must be below 100 (a 100% margin implies a zero cost)" };
8326
+ }
8327
+ if (has("markupPercent") && markup0 <= -100) {
8328
+ return { error: "Markup percent must be greater than -100" };
8329
+ }
8330
+ let cost;
8331
+ let price;
8332
+ if (has("cost") && has("price")) {
8333
+ cost = cost0;
8334
+ price = price0;
8335
+ } else if (has("cost") && has("marginPercent")) {
8336
+ cost = cost0;
8337
+ price = cost0 / (1 - margin0 / 100);
8338
+ } else if (has("cost") && has("markupPercent")) {
8339
+ cost = cost0;
8340
+ price = cost0 * (1 + markup0 / 100);
8341
+ } else if (has("price") && has("marginPercent")) {
8342
+ price = price0;
8343
+ cost = price0 * (1 - margin0 / 100);
8344
+ } else if (has("price") && has("markupPercent")) {
8345
+ price = price0;
8346
+ cost = price0 / (1 + markup0 / 100);
8347
+ } else {
8348
+ return { error: "Margin and markup alone cannot be solved: also provide a cost or a price" };
8349
+ }
8350
+ if (!Number.isFinite(cost) || !Number.isFinite(price)) return { error: "Inputs produce a non-finite result" };
8351
+ if (cost <= 0) return { error: "Those inputs imply a cost of zero or less" };
8352
+ if (price <= 0) return { error: "Those inputs imply a price of zero or less" };
8353
+ const round4 = (n) => Math.round(n * 1e4) / 1e4;
8354
+ const profit = price - cost;
8355
+ return {
8356
+ cost: round4(cost),
8357
+ price: round4(price),
8358
+ profit: round4(profit),
8359
+ marginPercent: round4(profit / price * 100),
8360
+ markupPercent: round4(profit / cost * 100),
8361
+ given,
8362
+ solvedFor: _MARGIN_FIELDS.filter((f) => !given.includes(f))
8363
+ };
8364
+ }
8365
+ function marginToMarkup(marginPercent) {
8366
+ if (!Number.isFinite(marginPercent) || marginPercent >= 100) {
8367
+ return { error: "Margin percent must be a finite number below 100" };
8368
+ }
8369
+ return Math.round(marginPercent / (100 - marginPercent) * 100 * 1e4) / 1e4;
8370
+ }
8371
+ function markupToMargin(markupPercent) {
8372
+ if (!Number.isFinite(markupPercent) || markupPercent <= -100) {
8373
+ return { error: "Markup percent must be a finite number greater than -100" };
8374
+ }
8375
+ return Math.round(markupPercent / (100 + markupPercent) * 100 * 1e4) / 1e4;
8376
+ }
8181
8377
 
8182
8378
  // src/tools/network.ts
8183
8379
  var network_exports = {};
package/dist/index.d.cts CHANGED
@@ -4,8 +4,8 @@ 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-pIRkHnsU.cjs';
7
+ export { t as time } from './time-KqJjdqII.cjs';
8
+ export { u as units } from './units-h4rHQGp5.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
@@ -4,8 +4,8 @@ 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-pIRkHnsU.js';
7
+ export { t as time } from './time-KqJjdqII.js';
8
+ export { u as units } from './units-h4rHQGp5.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-TFNCFD3E.js';
2
+ export { units_exports as units } from './chunk-AIKSKJ77.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';
@@ -13,7 +13,7 @@ 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