@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 +8 -0
- package/README.md +16 -3
- package/dist/{chunk-TFNCFD3E.js → chunk-AIKSKJ77.js} +104 -1
- package/dist/{chunk-ZPQZEIXP.js → chunk-IEPWZERQ.js} +95 -2
- package/dist/index.cjs +197 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/{time-DbT8fjaF.d.cts → time-KqJjdqII.d.cts} +48 -2
- package/dist/{time-DbT8fjaF.d.ts → time-KqJjdqII.d.ts} +48 -2
- package/dist/tools/time.cjs +93 -0
- package/dist/tools/time.d.cts +1 -1
- package/dist/tools/time.d.ts +1 -1
- package/dist/tools/time.js +1 -1
- package/dist/tools/units.cjs +103 -0
- package/dist/tools/units.d.cts +1 -1
- package/dist/tools/units.d.ts +1 -1
- package/dist/tools/units.js +1 -1
- package/dist/{units-pIRkHnsU.d.cts → units-h4rHQGp5.d.cts} +74 -2
- package/dist/{units-pIRkHnsU.d.ts → units-h4rHQGp5.d.ts} +74 -2
- package/package.json +1 -1
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
|
-
**
|
|
3
|
+
**564 developer utility functions for Node.js: runs locally, no API key required.**
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@utilix-tech/sdk)
|
|
6
6
|
[](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
|
|
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-
|
|
8
|
-
export { u as units } from './units-
|
|
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-
|
|
8
|
-
export { u as units } from './units-
|
|
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-
|
|
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-
|
|
16
|
+
export { time_exports as time } from './chunk-IEPWZERQ.js';
|
|
17
17
|
import './chunk-MLKGABMK.js';
|
|
18
18
|
|
|
19
19
|
// src/index.ts
|