@eristack/percent 0.1.1
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/LICENSE +21 -0
- package/README.md +11 -0
- package/dist/core/percent.d.ts +16 -0
- package/dist/core/percent.d.ts.map +1 -0
- package/dist/core/types.d.ts +16 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/index.cjs +128 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +82 -0
- package/dist/index.js.map +1 -0
- package/dist/zod/index.cjs +39 -0
- package/dist/zod/index.cjs.map +1 -0
- package/dist/zod/index.d.ts +2 -0
- package/dist/zod/index.d.ts.map +1 -0
- package/dist/zod/index.js +11 -0
- package/dist/zod/index.js.map +1 -0
- package/dist/zod/schemas.d.ts +8 -0
- package/dist/zod/schemas.d.ts.map +1 -0
- package/docs/_meta.json +50 -0
- package/docs/api-reference.md +37 -0
- package/docs/arithmetic.md +51 -0
- package/docs/basis-points.md +41 -0
- package/docs/concepts.md +40 -0
- package/docs/getting-started.md +50 -0
- package/docs/gotchas.md +32 -0
- package/docs/index.md +40 -0
- package/docs/recipes.md +44 -0
- package/docs/zod.md +20 -0
- package/package.json +83 -0
- package/skills/percent-core/SKILL.md +36 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) Eristack contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# @eristack/percent
|
|
2
|
+
|
|
3
|
+
Percent and basis-point ratios as strings — tax, discount, and markup rates without float literals.
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { parsePercent, percentOf } from "@eristack/percent";
|
|
7
|
+
|
|
8
|
+
percentOf("100", parsePercent("10%")); // "10"
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Docs: [packages/primitive/percent/docs/index.md](./docs/index.md)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Percent, PercentInput } from "./types.js";
|
|
2
|
+
export declare class PercentParseError extends Error {
|
|
3
|
+
constructor(message: string);
|
|
4
|
+
}
|
|
5
|
+
/** Parse "11%", "0.11", or basis points via PercentInput. */
|
|
6
|
+
export declare function parsePercent(input: PercentInput | string): Percent;
|
|
7
|
+
export declare function fromBasisPoints(bps: string): Percent;
|
|
8
|
+
export declare function fromPercentSymbol(value: string): Percent;
|
|
9
|
+
export declare function toBasisPoints(p: Percent): string;
|
|
10
|
+
export declare function toPercentSymbol(p: Percent): string;
|
|
11
|
+
/** amount * ratio — both decimal strings. */
|
|
12
|
+
export declare function percentOf(amount: string, percent: Percent): string;
|
|
13
|
+
export declare function plusPercent(amount: string, percent: Percent): string;
|
|
14
|
+
export declare function minusPercent(amount: string, percent: Percent): string;
|
|
15
|
+
export declare function addPercents(a: Percent, b: Percent): Percent;
|
|
16
|
+
//# sourceMappingURL=percent.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"percent.d.ts","sourceRoot":"","sources":["../../src/core/percent.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAExD,qBAAa,iBAAkB,SAAQ,KAAK;IAC1C,YAAY,OAAO,EAAE,MAAM,EAG1B;CACF;AASD,6DAA6D;AAC7D,wBAAgB,YAAY,CAAC,KAAK,EAAE,YAAY,GAAG,MAAM,GAAG,OAAO,CA8BlE;AAED,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAEpD;AAED,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAExD;AAED,wBAAgB,aAAa,CAAC,CAAC,EAAE,OAAO,GAAG,MAAM,CAEhD;AAED,wBAAgB,eAAe,CAAC,CAAC,EAAE,OAAO,GAAG,MAAM,CAElD;AAED,6CAA6C;AAC7C,wBAAgB,SAAS,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,MAAM,CAElE;AAED,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,MAAM,CAEpE;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,MAAM,CAErE;AAED,wBAAgB,WAAW,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,OAAO,GAAG,OAAO,CAE3D"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Stored as decimal ratio string (0.11 = 11%). Never use JS float literals. */
|
|
2
|
+
export type PercentValue = string;
|
|
3
|
+
export type Percent = {
|
|
4
|
+
readonly ratio: PercentValue;
|
|
5
|
+
};
|
|
6
|
+
export type PercentInput = {
|
|
7
|
+
kind: "ratio";
|
|
8
|
+
value: string;
|
|
9
|
+
} | {
|
|
10
|
+
kind: "percent";
|
|
11
|
+
value: string;
|
|
12
|
+
} | {
|
|
13
|
+
kind: "basisPoints";
|
|
14
|
+
value: string;
|
|
15
|
+
};
|
|
16
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/core/types.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC;AAElC,MAAM,MAAM,OAAO,GAAG;IACpB,QAAQ,CAAC,KAAK,EAAE,YAAY,CAAC;CAC9B,CAAC;AAEF,MAAM,MAAM,YAAY,GACpB;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAChC;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAClC;IAAE,IAAI,EAAE,aAAa,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC"}
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
PercentParseError: () => PercentParseError,
|
|
34
|
+
addPercents: () => addPercents,
|
|
35
|
+
fromBasisPoints: () => fromBasisPoints,
|
|
36
|
+
fromPercentSymbol: () => fromPercentSymbol,
|
|
37
|
+
minusPercent: () => minusPercent,
|
|
38
|
+
parsePercent: () => parsePercent,
|
|
39
|
+
percentOf: () => percentOf,
|
|
40
|
+
plusPercent: () => plusPercent,
|
|
41
|
+
toBasisPoints: () => toBasisPoints,
|
|
42
|
+
toPercentSymbol: () => toPercentSymbol
|
|
43
|
+
});
|
|
44
|
+
module.exports = __toCommonJS(index_exports);
|
|
45
|
+
|
|
46
|
+
// src/core/percent.ts
|
|
47
|
+
var import_decimal = __toESM(require("decimal.js"), 1);
|
|
48
|
+
var PercentParseError = class extends Error {
|
|
49
|
+
constructor(message) {
|
|
50
|
+
super(message);
|
|
51
|
+
this.name = "PercentParseError";
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
function normalizeRatio(ratio) {
|
|
55
|
+
if (ratio.isNegative()) {
|
|
56
|
+
throw new PercentParseError("Percent ratio cannot be negative");
|
|
57
|
+
}
|
|
58
|
+
return { ratio: ratio.toFixed() };
|
|
59
|
+
}
|
|
60
|
+
function parsePercent(input) {
|
|
61
|
+
if (typeof input === "string") {
|
|
62
|
+
const trimmed = input.trim();
|
|
63
|
+
if (!trimmed) {
|
|
64
|
+
throw new PercentParseError("Percent input cannot be empty");
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
if (trimmed.endsWith("%")) {
|
|
68
|
+
const n = trimmed.slice(0, -1).trim();
|
|
69
|
+
if (!n) throw new PercentParseError('Percent symbol missing value before "%"');
|
|
70
|
+
return normalizeRatio(new import_decimal.default(n).div(100));
|
|
71
|
+
}
|
|
72
|
+
return normalizeRatio(new import_decimal.default(trimmed));
|
|
73
|
+
} catch (err) {
|
|
74
|
+
if (err instanceof PercentParseError) throw err;
|
|
75
|
+
throw new PercentParseError(`Invalid percent "${input}"`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
switch (input.kind) {
|
|
79
|
+
case "ratio":
|
|
80
|
+
return normalizeRatio(new import_decimal.default(input.value));
|
|
81
|
+
case "percent":
|
|
82
|
+
return normalizeRatio(new import_decimal.default(input.value).div(100));
|
|
83
|
+
case "basisPoints":
|
|
84
|
+
return normalizeRatio(new import_decimal.default(input.value).div(1e4));
|
|
85
|
+
default: {
|
|
86
|
+
const _exhaustive = input;
|
|
87
|
+
throw new PercentParseError(`Unknown percent input ${String(_exhaustive)}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
function fromBasisPoints(bps) {
|
|
92
|
+
return parsePercent({ kind: "basisPoints", value: bps });
|
|
93
|
+
}
|
|
94
|
+
function fromPercentSymbol(value) {
|
|
95
|
+
return parsePercent({ kind: "percent", value });
|
|
96
|
+
}
|
|
97
|
+
function toBasisPoints(p) {
|
|
98
|
+
return new import_decimal.default(p.ratio).times(1e4).toFixed(0);
|
|
99
|
+
}
|
|
100
|
+
function toPercentSymbol(p) {
|
|
101
|
+
return `${new import_decimal.default(p.ratio).times(100).toFixed()}%`;
|
|
102
|
+
}
|
|
103
|
+
function percentOf(amount, percent) {
|
|
104
|
+
return new import_decimal.default(amount).times(percent.ratio).toFixed();
|
|
105
|
+
}
|
|
106
|
+
function plusPercent(amount, percent) {
|
|
107
|
+
return new import_decimal.default(amount).times(new import_decimal.default(1).plus(percent.ratio)).toFixed();
|
|
108
|
+
}
|
|
109
|
+
function minusPercent(amount, percent) {
|
|
110
|
+
return new import_decimal.default(amount).times(new import_decimal.default(1).minus(percent.ratio)).toFixed();
|
|
111
|
+
}
|
|
112
|
+
function addPercents(a, b) {
|
|
113
|
+
return { ratio: new import_decimal.default(a.ratio).plus(b.ratio).toFixed() };
|
|
114
|
+
}
|
|
115
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
116
|
+
0 && (module.exports = {
|
|
117
|
+
PercentParseError,
|
|
118
|
+
addPercents,
|
|
119
|
+
fromBasisPoints,
|
|
120
|
+
fromPercentSymbol,
|
|
121
|
+
minusPercent,
|
|
122
|
+
parsePercent,
|
|
123
|
+
percentOf,
|
|
124
|
+
plusPercent,
|
|
125
|
+
toBasisPoints,
|
|
126
|
+
toPercentSymbol
|
|
127
|
+
});
|
|
128
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/core/percent.ts"],"sourcesContent":["export {\n addPercents,\n fromBasisPoints,\n fromPercentSymbol,\n minusPercent,\n parsePercent,\n percentOf,\n PercentParseError,\n plusPercent,\n toBasisPoints,\n toPercentSymbol,\n} from \"./core/percent.js\";\nexport type { Percent, PercentInput, PercentValue } from \"./core/types.js\";\n","import Decimal from \"decimal.js\";\nimport type { Percent, PercentInput } from \"./types.js\";\n\nexport class PercentParseError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"PercentParseError\";\n }\n}\n\nfunction normalizeRatio(ratio: Decimal): Percent {\n if (ratio.isNegative()) {\n throw new PercentParseError(\"Percent ratio cannot be negative\");\n }\n return { ratio: ratio.toFixed() };\n}\n\n/** Parse \"11%\", \"0.11\", or basis points via PercentInput. */\nexport function parsePercent(input: PercentInput | string): Percent {\n if (typeof input === \"string\") {\n const trimmed = input.trim();\n if (!trimmed) {\n throw new PercentParseError(\"Percent input cannot be empty\");\n }\n try {\n if (trimmed.endsWith(\"%\")) {\n const n = trimmed.slice(0, -1).trim();\n if (!n) throw new PercentParseError('Percent symbol missing value before \"%\"');\n return normalizeRatio(new Decimal(n).div(100));\n }\n return normalizeRatio(new Decimal(trimmed));\n } catch (err) {\n if (err instanceof PercentParseError) throw err;\n throw new PercentParseError(`Invalid percent \"${input}\"`);\n }\n }\n switch (input.kind) {\n case \"ratio\":\n return normalizeRatio(new Decimal(input.value));\n case \"percent\":\n return normalizeRatio(new Decimal(input.value).div(100));\n case \"basisPoints\":\n return normalizeRatio(new Decimal(input.value).div(10_000));\n default: {\n const _exhaustive: never = input;\n throw new PercentParseError(`Unknown percent input ${String(_exhaustive)}`);\n }\n }\n}\n\nexport function fromBasisPoints(bps: string): Percent {\n return parsePercent({ kind: \"basisPoints\", value: bps });\n}\n\nexport function fromPercentSymbol(value: string): Percent {\n return parsePercent({ kind: \"percent\", value });\n}\n\nexport function toBasisPoints(p: Percent): string {\n return new Decimal(p.ratio).times(10_000).toFixed(0);\n}\n\nexport function toPercentSymbol(p: Percent): string {\n return `${new Decimal(p.ratio).times(100).toFixed()}%`;\n}\n\n/** amount * ratio — both decimal strings. */\nexport function percentOf(amount: string, percent: Percent): string {\n return new Decimal(amount).times(percent.ratio).toFixed();\n}\n\nexport function plusPercent(amount: string, percent: Percent): string {\n return new Decimal(amount).times(new Decimal(1).plus(percent.ratio)).toFixed();\n}\n\nexport function minusPercent(amount: string, percent: Percent): string {\n return new Decimal(amount).times(new Decimal(1).minus(percent.ratio)).toFixed();\n}\n\nexport function addPercents(a: Percent, b: Percent): Percent {\n return { ratio: new Decimal(a.ratio).plus(b.ratio).toFixed() };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,qBAAoB;AAGb,IAAM,oBAAN,cAAgC,MAAM;AAAA,EAC3C,YAAY,SAAiB;AAC3B,UAAM,OAAO;AACb,SAAK,OAAO;AAAA,EACd;AACF;AAEA,SAAS,eAAe,OAAyB;AAC/C,MAAI,MAAM,WAAW,GAAG;AACtB,UAAM,IAAI,kBAAkB,kCAAkC;AAAA,EAChE;AACA,SAAO,EAAE,OAAO,MAAM,QAAQ,EAAE;AAClC;AAGO,SAAS,aAAa,OAAuC;AAClE,MAAI,OAAO,UAAU,UAAU;AAC7B,UAAM,UAAU,MAAM,KAAK;AAC3B,QAAI,CAAC,SAAS;AACZ,YAAM,IAAI,kBAAkB,+BAA+B;AAAA,IAC7D;AACA,QAAI;AACF,UAAI,QAAQ,SAAS,GAAG,GAAG;AACzB,cAAM,IAAI,QAAQ,MAAM,GAAG,EAAE,EAAE,KAAK;AACpC,YAAI,CAAC,EAAG,OAAM,IAAI,kBAAkB,yCAAyC;AAC7E,eAAO,eAAe,IAAI,eAAAA,QAAQ,CAAC,EAAE,IAAI,GAAG,CAAC;AAAA,MAC/C;AACA,aAAO,eAAe,IAAI,eAAAA,QAAQ,OAAO,CAAC;AAAA,IAC5C,SAAS,KAAK;AACZ,UAAI,eAAe,kBAAmB,OAAM;AAC5C,YAAM,IAAI,kBAAkB,oBAAoB,KAAK,GAAG;AAAA,IAC1D;AAAA,EACF;AACA,UAAQ,MAAM,MAAM;AAAA,IAClB,KAAK;AACH,aAAO,eAAe,IAAI,eAAAA,QAAQ,MAAM,KAAK,CAAC;AAAA,IAChD,KAAK;AACH,aAAO,eAAe,IAAI,eAAAA,QAAQ,MAAM,KAAK,EAAE,IAAI,GAAG,CAAC;AAAA,IACzD,KAAK;AACH,aAAO,eAAe,IAAI,eAAAA,QAAQ,MAAM,KAAK,EAAE,IAAI,GAAM,CAAC;AAAA,IAC5D,SAAS;AACP,YAAM,cAAqB;AAC3B,YAAM,IAAI,kBAAkB,yBAAyB,OAAO,WAAW,CAAC,EAAE;AAAA,IAC5E;AAAA,EACF;AACF;AAEO,SAAS,gBAAgB,KAAsB;AACpD,SAAO,aAAa,EAAE,MAAM,eAAe,OAAO,IAAI,CAAC;AACzD;AAEO,SAAS,kBAAkB,OAAwB;AACxD,SAAO,aAAa,EAAE,MAAM,WAAW,MAAM,CAAC;AAChD;AAEO,SAAS,cAAc,GAAoB;AAChD,SAAO,IAAI,eAAAA,QAAQ,EAAE,KAAK,EAAE,MAAM,GAAM,EAAE,QAAQ,CAAC;AACrD;AAEO,SAAS,gBAAgB,GAAoB;AAClD,SAAO,GAAG,IAAI,eAAAA,QAAQ,EAAE,KAAK,EAAE,MAAM,GAAG,EAAE,QAAQ,CAAC;AACrD;AAGO,SAAS,UAAU,QAAgB,SAA0B;AAClE,SAAO,IAAI,eAAAA,QAAQ,MAAM,EAAE,MAAM,QAAQ,KAAK,EAAE,QAAQ;AAC1D;AAEO,SAAS,YAAY,QAAgB,SAA0B;AACpE,SAAO,IAAI,eAAAA,QAAQ,MAAM,EAAE,MAAM,IAAI,eAAAA,QAAQ,CAAC,EAAE,KAAK,QAAQ,KAAK,CAAC,EAAE,QAAQ;AAC/E;AAEO,SAAS,aAAa,QAAgB,SAA0B;AACrE,SAAO,IAAI,eAAAA,QAAQ,MAAM,EAAE,MAAM,IAAI,eAAAA,QAAQ,CAAC,EAAE,MAAM,QAAQ,KAAK,CAAC,EAAE,QAAQ;AAChF;AAEO,SAAS,YAAY,GAAY,GAAqB;AAC3D,SAAO,EAAE,OAAO,IAAI,eAAAA,QAAQ,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE;AAC/D;","names":["Decimal"]}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { addPercents, fromBasisPoints, fromPercentSymbol, minusPercent, parsePercent, percentOf, PercentParseError, plusPercent, toBasisPoints, toPercentSymbol, } from "./core/percent.js";
|
|
2
|
+
export type { Percent, PercentInput, PercentValue } from "./core/types.js";
|
|
3
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,WAAW,EACX,eAAe,EACf,iBAAiB,EACjB,YAAY,EACZ,YAAY,EACZ,SAAS,EACT,iBAAiB,EACjB,WAAW,EACX,aAAa,EACb,eAAe,GAChB,MAAM,mBAAmB,CAAC;AAC3B,YAAY,EAAE,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// src/core/percent.ts
|
|
2
|
+
import Decimal from "decimal.js";
|
|
3
|
+
var PercentParseError = class extends Error {
|
|
4
|
+
constructor(message) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = "PercentParseError";
|
|
7
|
+
}
|
|
8
|
+
};
|
|
9
|
+
function normalizeRatio(ratio) {
|
|
10
|
+
if (ratio.isNegative()) {
|
|
11
|
+
throw new PercentParseError("Percent ratio cannot be negative");
|
|
12
|
+
}
|
|
13
|
+
return { ratio: ratio.toFixed() };
|
|
14
|
+
}
|
|
15
|
+
function parsePercent(input) {
|
|
16
|
+
if (typeof input === "string") {
|
|
17
|
+
const trimmed = input.trim();
|
|
18
|
+
if (!trimmed) {
|
|
19
|
+
throw new PercentParseError("Percent input cannot be empty");
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
if (trimmed.endsWith("%")) {
|
|
23
|
+
const n = trimmed.slice(0, -1).trim();
|
|
24
|
+
if (!n) throw new PercentParseError('Percent symbol missing value before "%"');
|
|
25
|
+
return normalizeRatio(new Decimal(n).div(100));
|
|
26
|
+
}
|
|
27
|
+
return normalizeRatio(new Decimal(trimmed));
|
|
28
|
+
} catch (err) {
|
|
29
|
+
if (err instanceof PercentParseError) throw err;
|
|
30
|
+
throw new PercentParseError(`Invalid percent "${input}"`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
switch (input.kind) {
|
|
34
|
+
case "ratio":
|
|
35
|
+
return normalizeRatio(new Decimal(input.value));
|
|
36
|
+
case "percent":
|
|
37
|
+
return normalizeRatio(new Decimal(input.value).div(100));
|
|
38
|
+
case "basisPoints":
|
|
39
|
+
return normalizeRatio(new Decimal(input.value).div(1e4));
|
|
40
|
+
default: {
|
|
41
|
+
const _exhaustive = input;
|
|
42
|
+
throw new PercentParseError(`Unknown percent input ${String(_exhaustive)}`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function fromBasisPoints(bps) {
|
|
47
|
+
return parsePercent({ kind: "basisPoints", value: bps });
|
|
48
|
+
}
|
|
49
|
+
function fromPercentSymbol(value) {
|
|
50
|
+
return parsePercent({ kind: "percent", value });
|
|
51
|
+
}
|
|
52
|
+
function toBasisPoints(p) {
|
|
53
|
+
return new Decimal(p.ratio).times(1e4).toFixed(0);
|
|
54
|
+
}
|
|
55
|
+
function toPercentSymbol(p) {
|
|
56
|
+
return `${new Decimal(p.ratio).times(100).toFixed()}%`;
|
|
57
|
+
}
|
|
58
|
+
function percentOf(amount, percent) {
|
|
59
|
+
return new Decimal(amount).times(percent.ratio).toFixed();
|
|
60
|
+
}
|
|
61
|
+
function plusPercent(amount, percent) {
|
|
62
|
+
return new Decimal(amount).times(new Decimal(1).plus(percent.ratio)).toFixed();
|
|
63
|
+
}
|
|
64
|
+
function minusPercent(amount, percent) {
|
|
65
|
+
return new Decimal(amount).times(new Decimal(1).minus(percent.ratio)).toFixed();
|
|
66
|
+
}
|
|
67
|
+
function addPercents(a, b) {
|
|
68
|
+
return { ratio: new Decimal(a.ratio).plus(b.ratio).toFixed() };
|
|
69
|
+
}
|
|
70
|
+
export {
|
|
71
|
+
PercentParseError,
|
|
72
|
+
addPercents,
|
|
73
|
+
fromBasisPoints,
|
|
74
|
+
fromPercentSymbol,
|
|
75
|
+
minusPercent,
|
|
76
|
+
parsePercent,
|
|
77
|
+
percentOf,
|
|
78
|
+
plusPercent,
|
|
79
|
+
toBasisPoints,
|
|
80
|
+
toPercentSymbol
|
|
81
|
+
};
|
|
82
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/core/percent.ts"],"sourcesContent":["import Decimal from \"decimal.js\";\nimport type { Percent, PercentInput } from \"./types.js\";\n\nexport class PercentParseError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"PercentParseError\";\n }\n}\n\nfunction normalizeRatio(ratio: Decimal): Percent {\n if (ratio.isNegative()) {\n throw new PercentParseError(\"Percent ratio cannot be negative\");\n }\n return { ratio: ratio.toFixed() };\n}\n\n/** Parse \"11%\", \"0.11\", or basis points via PercentInput. */\nexport function parsePercent(input: PercentInput | string): Percent {\n if (typeof input === \"string\") {\n const trimmed = input.trim();\n if (!trimmed) {\n throw new PercentParseError(\"Percent input cannot be empty\");\n }\n try {\n if (trimmed.endsWith(\"%\")) {\n const n = trimmed.slice(0, -1).trim();\n if (!n) throw new PercentParseError('Percent symbol missing value before \"%\"');\n return normalizeRatio(new Decimal(n).div(100));\n }\n return normalizeRatio(new Decimal(trimmed));\n } catch (err) {\n if (err instanceof PercentParseError) throw err;\n throw new PercentParseError(`Invalid percent \"${input}\"`);\n }\n }\n switch (input.kind) {\n case \"ratio\":\n return normalizeRatio(new Decimal(input.value));\n case \"percent\":\n return normalizeRatio(new Decimal(input.value).div(100));\n case \"basisPoints\":\n return normalizeRatio(new Decimal(input.value).div(10_000));\n default: {\n const _exhaustive: never = input;\n throw new PercentParseError(`Unknown percent input ${String(_exhaustive)}`);\n }\n }\n}\n\nexport function fromBasisPoints(bps: string): Percent {\n return parsePercent({ kind: \"basisPoints\", value: bps });\n}\n\nexport function fromPercentSymbol(value: string): Percent {\n return parsePercent({ kind: \"percent\", value });\n}\n\nexport function toBasisPoints(p: Percent): string {\n return new Decimal(p.ratio).times(10_000).toFixed(0);\n}\n\nexport function toPercentSymbol(p: Percent): string {\n return `${new Decimal(p.ratio).times(100).toFixed()}%`;\n}\n\n/** amount * ratio — both decimal strings. */\nexport function percentOf(amount: string, percent: Percent): string {\n return new Decimal(amount).times(percent.ratio).toFixed();\n}\n\nexport function plusPercent(amount: string, percent: Percent): string {\n return new Decimal(amount).times(new Decimal(1).plus(percent.ratio)).toFixed();\n}\n\nexport function minusPercent(amount: string, percent: Percent): string {\n return new Decimal(amount).times(new Decimal(1).minus(percent.ratio)).toFixed();\n}\n\nexport function addPercents(a: Percent, b: Percent): Percent {\n return { ratio: new Decimal(a.ratio).plus(b.ratio).toFixed() };\n}\n"],"mappings":";AAAA,OAAO,aAAa;AAGb,IAAM,oBAAN,cAAgC,MAAM;AAAA,EAC3C,YAAY,SAAiB;AAC3B,UAAM,OAAO;AACb,SAAK,OAAO;AAAA,EACd;AACF;AAEA,SAAS,eAAe,OAAyB;AAC/C,MAAI,MAAM,WAAW,GAAG;AACtB,UAAM,IAAI,kBAAkB,kCAAkC;AAAA,EAChE;AACA,SAAO,EAAE,OAAO,MAAM,QAAQ,EAAE;AAClC;AAGO,SAAS,aAAa,OAAuC;AAClE,MAAI,OAAO,UAAU,UAAU;AAC7B,UAAM,UAAU,MAAM,KAAK;AAC3B,QAAI,CAAC,SAAS;AACZ,YAAM,IAAI,kBAAkB,+BAA+B;AAAA,IAC7D;AACA,QAAI;AACF,UAAI,QAAQ,SAAS,GAAG,GAAG;AACzB,cAAM,IAAI,QAAQ,MAAM,GAAG,EAAE,EAAE,KAAK;AACpC,YAAI,CAAC,EAAG,OAAM,IAAI,kBAAkB,yCAAyC;AAC7E,eAAO,eAAe,IAAI,QAAQ,CAAC,EAAE,IAAI,GAAG,CAAC;AAAA,MAC/C;AACA,aAAO,eAAe,IAAI,QAAQ,OAAO,CAAC;AAAA,IAC5C,SAAS,KAAK;AACZ,UAAI,eAAe,kBAAmB,OAAM;AAC5C,YAAM,IAAI,kBAAkB,oBAAoB,KAAK,GAAG;AAAA,IAC1D;AAAA,EACF;AACA,UAAQ,MAAM,MAAM;AAAA,IAClB,KAAK;AACH,aAAO,eAAe,IAAI,QAAQ,MAAM,KAAK,CAAC;AAAA,IAChD,KAAK;AACH,aAAO,eAAe,IAAI,QAAQ,MAAM,KAAK,EAAE,IAAI,GAAG,CAAC;AAAA,IACzD,KAAK;AACH,aAAO,eAAe,IAAI,QAAQ,MAAM,KAAK,EAAE,IAAI,GAAM,CAAC;AAAA,IAC5D,SAAS;AACP,YAAM,cAAqB;AAC3B,YAAM,IAAI,kBAAkB,yBAAyB,OAAO,WAAW,CAAC,EAAE;AAAA,IAC5E;AAAA,EACF;AACF;AAEO,SAAS,gBAAgB,KAAsB;AACpD,SAAO,aAAa,EAAE,MAAM,eAAe,OAAO,IAAI,CAAC;AACzD;AAEO,SAAS,kBAAkB,OAAwB;AACxD,SAAO,aAAa,EAAE,MAAM,WAAW,MAAM,CAAC;AAChD;AAEO,SAAS,cAAc,GAAoB;AAChD,SAAO,IAAI,QAAQ,EAAE,KAAK,EAAE,MAAM,GAAM,EAAE,QAAQ,CAAC;AACrD;AAEO,SAAS,gBAAgB,GAAoB;AAClD,SAAO,GAAG,IAAI,QAAQ,EAAE,KAAK,EAAE,MAAM,GAAG,EAAE,QAAQ,CAAC;AACrD;AAGO,SAAS,UAAU,QAAgB,SAA0B;AAClE,SAAO,IAAI,QAAQ,MAAM,EAAE,MAAM,QAAQ,KAAK,EAAE,QAAQ;AAC1D;AAEO,SAAS,YAAY,QAAgB,SAA0B;AACpE,SAAO,IAAI,QAAQ,MAAM,EAAE,MAAM,IAAI,QAAQ,CAAC,EAAE,KAAK,QAAQ,KAAK,CAAC,EAAE,QAAQ;AAC/E;AAEO,SAAS,aAAa,QAAgB,SAA0B;AACrE,SAAO,IAAI,QAAQ,MAAM,EAAE,MAAM,IAAI,QAAQ,CAAC,EAAE,MAAM,QAAQ,KAAK,CAAC,EAAE,QAAQ;AAChF;AAEO,SAAS,YAAY,GAAY,GAAqB;AAC3D,SAAO,EAAE,OAAO,IAAI,QAAQ,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE;AAC/D;","names":[]}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/zod/index.ts
|
|
21
|
+
var zod_exports = {};
|
|
22
|
+
__export(zod_exports, {
|
|
23
|
+
percentRatioSchema: () => percentRatioSchema,
|
|
24
|
+
percentSchema: () => percentSchema
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(zod_exports);
|
|
27
|
+
|
|
28
|
+
// src/zod/schemas.ts
|
|
29
|
+
var import_zod = require("zod");
|
|
30
|
+
var percentRatioSchema = import_zod.z.string().min(1);
|
|
31
|
+
var percentSchema = import_zod.z.object({
|
|
32
|
+
ratio: percentRatioSchema
|
|
33
|
+
});
|
|
34
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
35
|
+
0 && (module.exports = {
|
|
36
|
+
percentRatioSchema,
|
|
37
|
+
percentSchema
|
|
38
|
+
});
|
|
39
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/zod/index.ts","../../src/zod/schemas.ts"],"sourcesContent":["export {\n percentRatioSchema,\n percentSchema,\n type PercentJson,\n} from \"./schemas.js\";\n","import { z } from \"zod\";\n\n/** Decimal ratio 0–1+ (e.g. \"0.11\" for 11%). */\nexport const percentRatioSchema = z.string().min(1);\n\nexport const percentSchema = z.object({\n ratio: percentRatioSchema,\n});\n\nexport type PercentJson = z.infer<typeof percentSchema>;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,iBAAkB;AAGX,IAAM,qBAAqB,aAAE,OAAO,EAAE,IAAI,CAAC;AAE3C,IAAM,gBAAgB,aAAE,OAAO;AAAA,EACpC,OAAO;AACT,CAAC;","names":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/zod/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,kBAAkB,EAClB,aAAa,EACb,KAAK,WAAW,GACjB,MAAM,cAAc,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/zod/schemas.ts"],"sourcesContent":["import { z } from \"zod\";\n\n/** Decimal ratio 0–1+ (e.g. \"0.11\" for 11%). */\nexport const percentRatioSchema = z.string().min(1);\n\nexport const percentSchema = z.object({\n ratio: percentRatioSchema,\n});\n\nexport type PercentJson = z.infer<typeof percentSchema>;\n"],"mappings":";AAAA,SAAS,SAAS;AAGX,IAAM,qBAAqB,EAAE,OAAO,EAAE,IAAI,CAAC;AAE3C,IAAM,gBAAgB,EAAE,OAAO;AAAA,EACpC,OAAO;AACT,CAAC;","names":[]}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/** Decimal ratio 0–1+ (e.g. "0.11" for 11%). */
|
|
3
|
+
export declare const percentRatioSchema: z.ZodString;
|
|
4
|
+
export declare const percentSchema: z.ZodObject<{
|
|
5
|
+
ratio: z.ZodString;
|
|
6
|
+
}, z.core.$strip>;
|
|
7
|
+
export type PercentJson = z.infer<typeof percentSchema>;
|
|
8
|
+
//# sourceMappingURL=schemas.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schemas.d.ts","sourceRoot":"","sources":["../../src/zod/schemas.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,gDAAgD;AAChD,eAAO,MAAM,kBAAkB,aAAoB,CAAC;AAEpD,eAAO,MAAM,aAAa;;iBAExB,CAAC;AAEH,MAAM,MAAM,WAAW,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,aAAa,CAAC,CAAC"}
|
package/docs/_meta.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"title": "@eristack/percent",
|
|
3
|
+
"pages": [
|
|
4
|
+
"index",
|
|
5
|
+
"getting-started",
|
|
6
|
+
"concepts",
|
|
7
|
+
"basis-points",
|
|
8
|
+
"arithmetic",
|
|
9
|
+
"zod",
|
|
10
|
+
"gotchas",
|
|
11
|
+
"recipes",
|
|
12
|
+
"api-reference"
|
|
13
|
+
],
|
|
14
|
+
"sections": [
|
|
15
|
+
{
|
|
16
|
+
"label": "Start",
|
|
17
|
+
"pages": [
|
|
18
|
+
"index",
|
|
19
|
+
"getting-started"
|
|
20
|
+
]
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"label": "Guides",
|
|
24
|
+
"pages": [
|
|
25
|
+
"concepts",
|
|
26
|
+
"basis-points",
|
|
27
|
+
"arithmetic"
|
|
28
|
+
]
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"label": "Adapters",
|
|
32
|
+
"pages": [
|
|
33
|
+
"zod"
|
|
34
|
+
]
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"label": "Guides",
|
|
38
|
+
"pages": [
|
|
39
|
+
"gotchas"
|
|
40
|
+
]
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"label": "Reference",
|
|
44
|
+
"pages": [
|
|
45
|
+
"recipes",
|
|
46
|
+
"api-reference"
|
|
47
|
+
]
|
|
48
|
+
}
|
|
49
|
+
]
|
|
50
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# API reference
|
|
2
|
+
|
|
3
|
+
## Types
|
|
4
|
+
|
|
5
|
+
| Export | Description |
|
|
6
|
+
| --- | --- |
|
|
7
|
+
| `Percent` | `{ ratio: string }` |
|
|
8
|
+
| `PercentValue` | Ratio string alias |
|
|
9
|
+
| `PercentInput` | Discriminated parse input |
|
|
10
|
+
|
|
11
|
+
## Parse & construct
|
|
12
|
+
|
|
13
|
+
| Export | Description |
|
|
14
|
+
| --- | --- |
|
|
15
|
+
| `parsePercent(input)` | String or PercentInput → Percent |
|
|
16
|
+
| `fromBasisPoints(bps)` | bps string → Percent |
|
|
17
|
+
| `fromPercentSymbol(value)` | `"11"` → 11% |
|
|
18
|
+
| `toBasisPoints(p)` | Percent → bps string |
|
|
19
|
+
| `toPercentSymbol(p)` | Display `"11%"` |
|
|
20
|
+
| `PercentParseError` | Invalid/negative input |
|
|
21
|
+
|
|
22
|
+
## Arithmetic
|
|
23
|
+
|
|
24
|
+
| Export | Description |
|
|
25
|
+
| --- | --- |
|
|
26
|
+
| `percentOf(amount, percent)` | amount × ratio |
|
|
27
|
+
| `plusPercent(amount, percent)` | amount × (1 + ratio) |
|
|
28
|
+
| `minusPercent(amount, percent)` | amount × (1 − ratio) |
|
|
29
|
+
| `addPercents(a, b)` | Sum ratios |
|
|
30
|
+
|
|
31
|
+
## Zod (`@eristack/percent/zod`)
|
|
32
|
+
|
|
33
|
+
| Export | Description |
|
|
34
|
+
| --- | --- |
|
|
35
|
+
| `percentSchema` | `{ ratio }` object |
|
|
36
|
+
| `percentRatioSchema` | Ratio string |
|
|
37
|
+
| `PercentJson` | Inferred type |
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Arithmetic
|
|
2
|
+
|
|
3
|
+
## percentOf
|
|
4
|
+
|
|
5
|
+
Line extension before tax:
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
percentOf("250", parsePercent("10%")); // "25"
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Both operands are decimal strings; result is decimal string (not rounded currency).
|
|
12
|
+
|
|
13
|
+
## plusPercent / minusPercent
|
|
14
|
+
|
|
15
|
+
Gross-up and discount base:
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
plusPercent("100", parsePercent("10%")); // "110" — add 10%
|
|
19
|
+
minusPercent("100", parsePercent("10%")); // "90" — subtract 10%
|
|
20
|
+
minusPercent("250", parsePercent("100%")); // "0" — full write-off of base
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
`minusPercent` at **100%** zeroes the base string — use before wrapping in `@eristack/money` for credit memos.
|
|
24
|
+
|
|
25
|
+
## addPercents
|
|
26
|
+
|
|
27
|
+
Combine component rates only when policy allows simple addition (e.g. stacked surcharges):
|
|
28
|
+
|
|
29
|
+
```ts
|
|
30
|
+
addPercents(parsePercent("5%"), parsePercent("2%")); // { ratio: "0.07" }
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Tax-on-tax and compound discounts need app rules — do not assume `addPercents` matches statutory calculation.
|
|
34
|
+
|
|
35
|
+
## Order with money rounding
|
|
36
|
+
|
|
37
|
+
1. Compute line subtotal as string (QUPS / money)
|
|
38
|
+
2. `percentOf(subtotal, taxRate)` → tax string
|
|
39
|
+
3. `Money.of(tax, currency)` + `round` at boundary
|
|
40
|
+
|
|
41
|
+
Applying percent after float conversion defeats the purpose of this package.
|
|
42
|
+
|
|
43
|
+
## toPercentSymbol
|
|
44
|
+
|
|
45
|
+
Display only:
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
toPercentSymbol(parsePercent("0.11")); // "11%"
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Do not parse display strings back from formatted UI without user intent.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Basis points
|
|
2
|
+
|
|
3
|
+
Finance and ERP tables often store **basis points (bps)** — 1 bps = 0.01%.
|
|
4
|
+
|
|
5
|
+
| Display | bps | ratio |
|
|
6
|
+
| --- | --- | --- |
|
|
7
|
+
| 11.00% | `1100` | `0.11` |
|
|
8
|
+
| 7.50% | `750` | `0.075` |
|
|
9
|
+
| 0.125% | `12.5` → use string `12.5` or store ratio | `0.00125` |
|
|
10
|
+
|
|
11
|
+
## API
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { fromBasisPoints, toBasisPoints, parsePercent } from "@eristack/percent";
|
|
15
|
+
|
|
16
|
+
fromBasisPoints("1100"); // { ratio: "0.11" }
|
|
17
|
+
toBasisPoints(parsePercent("11%")); // "1100"
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Drizzle column
|
|
21
|
+
|
|
22
|
+
Store bps as `numeric` or text in SQL; read as string into `fromBasisPoints`:
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
const rate = fromBasisPoints(row.vatBps);
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Precision
|
|
29
|
+
|
|
30
|
+
`toBasisPoints` uses `toFixed(0)` — suitable for whole bps. Sub-bps precision: store ratio string directly instead of bps.
|
|
31
|
+
|
|
32
|
+
## VAT / withholding tables
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
const rates = {
|
|
36
|
+
standard: fromBasisPoints("2000"), // 20%
|
|
37
|
+
reduced: fromBasisPoints("550"), // 5.5%
|
|
38
|
+
};
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Keep table in app DB; percent package only parses/applies.
|
package/docs/concepts.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Concepts
|
|
2
|
+
|
|
3
|
+
## Stored form: ratio
|
|
4
|
+
|
|
5
|
+
Internally `Percent` is `{ ratio: string }` where ratio is a decimal:
|
|
6
|
+
|
|
7
|
+
| Meaning | ratio string |
|
|
8
|
+
| --- | --- |
|
|
9
|
+
| 11% | `"0.11"` |
|
|
10
|
+
| 7.5% | `"0.075"` |
|
|
11
|
+
| 100% | `"1"` |
|
|
12
|
+
|
|
13
|
+
Never store `11` meaning 11% without parsing — use `parsePercent("11%")` or `fromBasisPoints`.
|
|
14
|
+
|
|
15
|
+
## Input kinds
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
parsePercent("11%"); // percent symbol
|
|
19
|
+
parsePercent("0.11"); // raw ratio
|
|
20
|
+
parsePercent({ kind: "basisPoints", value: "1100" }); // bps
|
|
21
|
+
parsePercent({ kind: "percent", value: "11" });
|
|
22
|
+
parsePercent({ kind: "ratio", value: "0.11" });
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## vs @eristack/money percent helpers
|
|
26
|
+
|
|
27
|
+
| Use | Package |
|
|
28
|
+
| --- | --- |
|
|
29
|
+
| Rate is master data / tax code | `@eristack/percent` |
|
|
30
|
+
| Apply known % to `Money` with currency | `@eristack/money` `percentOf` |
|
|
31
|
+
|
|
32
|
+
Typical flow: `percentOf(lineAmount, taxRate)` on strings → wrap in `Money.of` → round at ledger.
|
|
33
|
+
|
|
34
|
+
## Immutability
|
|
35
|
+
|
|
36
|
+
`Percent` objects are plain data — create new via `parsePercent` / `fromBasisPoints`; `addPercents` returns new ratio.
|
|
37
|
+
|
|
38
|
+
## Negative rates
|
|
39
|
+
|
|
40
|
+
Parsing rejects negative ratios. Credits/reversals use positive rate + app sign on amount.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Getting started
|
|
2
|
+
|
|
3
|
+
Parse and apply tax/discount rates without float literals.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @eristack/percent
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Parse rates
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { parsePercent, fromBasisPoints, percentOf } from "@eristack/percent";
|
|
15
|
+
|
|
16
|
+
parsePercent("11%"); // { ratio: "0.11" }
|
|
17
|
+
fromBasisPoints("1100"); // { ratio: "0.11" }
|
|
18
|
+
parsePercent({ kind: "ratio", value: "0.075" }); // 7.5%
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Apply to amounts
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
const vat = parsePercent("10%");
|
|
25
|
+
percentOf("100", vat); // "10"
|
|
26
|
+
plusPercent("100", vat); // "110"
|
|
27
|
+
minusPercent("100", vat); // "90"
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Round with `@eristack/money` at invoice boundaries after percent math on strings.
|
|
31
|
+
|
|
32
|
+
## Zod
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import { percentSchema } from "@eristack/percent/zod";
|
|
36
|
+
|
|
37
|
+
percentSchema.parse({ ratio: "0.11" });
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Production path
|
|
41
|
+
|
|
42
|
+
1. Store rates in master data as basis points or ratio strings.
|
|
43
|
+
2. Parse with `@eristack/percent` at API boundary.
|
|
44
|
+
3. Pass ratio into QUPS modifiers or money operators after line calculation.
|
|
45
|
+
|
|
46
|
+
## Next
|
|
47
|
+
|
|
48
|
+
- [Basis points](./basis-points.md) — finance tables
|
|
49
|
+
- [Arithmetic](./arithmetic.md) — combining rates
|
|
50
|
+
- [Recipes](./recipes.md) — tax line, stacked discount
|
package/docs/gotchas.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Gotchas
|
|
2
|
+
|
|
3
|
+
## Double scaling
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
percentOf("100", parsePercent("0.11")); // wrong if user meant 11%
|
|
7
|
+
percentOf("100", parsePercent("11%")); // correct
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## Mixing money Percent and this package
|
|
11
|
+
|
|
12
|
+
`@eristack/money` `percentOf(money, "7")` means **7 percent points**. `@eristack/percent` stores ratio `0.07`. Convert explicitly.
|
|
13
|
+
|
|
14
|
+
## Compound tax
|
|
15
|
+
|
|
16
|
+
`addPercents` is not compound tax (11% + 5% on top of taxed amount). Implement compound rules in app.
|
|
17
|
+
|
|
18
|
+
## Empty and malformed input
|
|
19
|
+
|
|
20
|
+
`parsePercent("")`, `parsePercent("%")`, and `parsePercent("abc%")` throw **`PercentParseError`** with a clear message — not raw Decimal errors.
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
parsePercent(0.11); // wrong type — use string
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Rounding direction
|
|
27
|
+
|
|
28
|
+
percent package does not apply banker's rounding — delegate to `@eristack/money` `Rounding` at invoice total.
|
|
29
|
+
|
|
30
|
+
## Rates over 100%
|
|
31
|
+
|
|
32
|
+
Ratio `"1.5"` (150%) parses if non-negative — allow in surcharge scenarios; reject in UI if business forbids.
|
package/docs/index.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "@eristack/percent"
|
|
3
|
+
description: Percent and basis-point ratios as strings for tax and discounts
|
|
4
|
+
sidebar_position: 1
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# @eristack/percent
|
|
8
|
+
|
|
9
|
+
`@eristack/percent` stores **rates and ratios as decimal strings** — `"0.11"` for 11%, basis points `"1100"` for 11.00% — so tax, discount, and markup math never touches JS float literals. Complements `@eristack/money` (amounts) and `@eristack/qups` (line calculators).
|
|
10
|
+
|
|
11
|
+
## When to use it
|
|
12
|
+
|
|
13
|
+
Use this package when you need:
|
|
14
|
+
|
|
15
|
+
- Parse `"11%"`, `"0.11"`, or basis points from ERP master data
|
|
16
|
+
- `percentOf`, `plusPercent`, `minusPercent` on string amounts before rounding with `@eristack/money`
|
|
17
|
+
- Serializable `{ ratio }` JSON for API contracts
|
|
18
|
+
- Zod 4 validation for percent fields
|
|
19
|
+
|
|
20
|
+
## Relationship to @eristack/money
|
|
21
|
+
|
|
22
|
+
`@eristack/money` includes `percentOf` on `Money` for **currency-safe** totals after you know the rate. Use `@eristack/percent` when the rate itself is the domain value (tax codes, tier tables, QUPS modifiers) and you want a dedicated ratio type.
|
|
23
|
+
|
|
24
|
+
## Subpaths
|
|
25
|
+
|
|
26
|
+
```text
|
|
27
|
+
@eristack/percent core — parsePercent, fromBasisPoints, percentOf
|
|
28
|
+
└── /zod percentSchema (peer zod ^4)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Next steps
|
|
32
|
+
|
|
33
|
+
- [Getting started](./getting-started.md) — parse, apply to line amount
|
|
34
|
+
- [Concepts](./concepts.md) — ratio vs percent symbol vs bps
|
|
35
|
+
- [Basis points](./basis-points.md) — finance rates, VAT tables
|
|
36
|
+
- [Arithmetic](./arithmetic.md) — percentOf, plus/minus, combining rates
|
|
37
|
+
- [Zod](./zod.md) — wire validation
|
|
38
|
+
- [Gotchas](./gotchas.md) — double-scaling, negative rates, money rounding order
|
|
39
|
+
- [Recipes](./recipes.md) — tax line, stacked discount, QUPS modifier
|
|
40
|
+
- [API reference](./api-reference.md) — exports cheat-sheet
|
package/docs/recipes.md
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Recipes
|
|
2
|
+
|
|
3
|
+
## Invoice VAT line
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { parsePercent, percentOf } from "@eristack/percent";
|
|
7
|
+
import { Money } from "@eristack/money";
|
|
8
|
+
|
|
9
|
+
const vat = parsePercent({ kind: "basisPoints", value: row.vatBps });
|
|
10
|
+
const taxStr = percentOf(subtotal.toDecimal(), vat);
|
|
11
|
+
const tax = Money.of(taxStr, subtotal.currency);
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Header discount
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
import { minusPercent, parsePercent } from "@eristack/percent";
|
|
18
|
+
|
|
19
|
+
const discounted = minusPercent(lineTotal, parsePercent("15%"));
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## QUPS modifier rate
|
|
23
|
+
|
|
24
|
+
Pass ratio string into modifier config; parse once at document load:
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
const modifierRate = parsePercent(taxCode.rate); // "10%" from master
|
|
28
|
+
// apply in line calculator before money wrap
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## API: accept human input
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
const rate = parsePercent(req.body.rateLabel); // "7.5%"
|
|
35
|
+
await db.update(taxCodes).set({ ratio: rate.ratio });
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Display bps in admin grid
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
import { toBasisPoints, parsePercent } from "@eristack/percent";
|
|
42
|
+
|
|
43
|
+
toBasisPoints(parsePercent(row.ratio)); // show in grid column
|
|
44
|
+
```
|
package/docs/zod.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Zod
|
|
2
|
+
|
|
3
|
+
Peer `zod ^4`.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pnpm add @eristack/percent @eristack/percent/zod zod
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { percentSchema, percentRatioSchema } from "@eristack/percent/zod";
|
|
11
|
+
|
|
12
|
+
percentSchema.parse({ ratio: "0.11" });
|
|
13
|
+
percentRatioSchema.parse("0.075");
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Validate shape on wire JSON; use `parsePercent` for `"11%"` human input forms.
|
|
17
|
+
|
|
18
|
+
## Type
|
|
19
|
+
|
|
20
|
+
`PercentJson` — `{ ratio: string }` for OpenAPI schemas.
|
package/package.json
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@eristack/percent",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Percent and basis-point ratios as strings — tax, discount, markup without float literals",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/eristack/business-libs.git",
|
|
8
|
+
"directory": "packages/primitive/percent"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/eristack/business-libs/tree/main/packages/primitive/percent",
|
|
11
|
+
"type": "module",
|
|
12
|
+
"main": "./dist/index.cjs",
|
|
13
|
+
"module": "./dist/index.js",
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"import": {
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"default": "./dist/index.js"
|
|
20
|
+
},
|
|
21
|
+
"require": {
|
|
22
|
+
"types": "./dist/index.d.ts",
|
|
23
|
+
"default": "./dist/index.cjs"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"./zod": {
|
|
27
|
+
"import": {
|
|
28
|
+
"types": "./dist/zod/index.d.ts",
|
|
29
|
+
"default": "./dist/zod/index.js"
|
|
30
|
+
},
|
|
31
|
+
"require": {
|
|
32
|
+
"types": "./dist/zod/index.d.ts",
|
|
33
|
+
"default": "./dist/zod/index.cjs"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"files": [
|
|
38
|
+
"dist",
|
|
39
|
+
"docs",
|
|
40
|
+
"README.md",
|
|
41
|
+
"skills",
|
|
42
|
+
"LICENSE"
|
|
43
|
+
],
|
|
44
|
+
"keywords": [
|
|
45
|
+
"tanstack-intent",
|
|
46
|
+
"percent",
|
|
47
|
+
"basis-points",
|
|
48
|
+
"tax",
|
|
49
|
+
"discount",
|
|
50
|
+
"eristack"
|
|
51
|
+
],
|
|
52
|
+
"publishConfig": {
|
|
53
|
+
"access": "public"
|
|
54
|
+
},
|
|
55
|
+
"dependencies": {
|
|
56
|
+
"decimal.js": "^10.6.0"
|
|
57
|
+
},
|
|
58
|
+
"peerDependencies": {
|
|
59
|
+
"zod": "^4.0.0"
|
|
60
|
+
},
|
|
61
|
+
"peerDependenciesMeta": {
|
|
62
|
+
"zod": {
|
|
63
|
+
"optional": true
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
"devDependencies": {
|
|
67
|
+
"@tanstack/intent": "^0.3.6",
|
|
68
|
+
"@types/node": "^22.15.29",
|
|
69
|
+
"tsup": "^8.5.0",
|
|
70
|
+
"typescript": "^7.0.0",
|
|
71
|
+
"vitest": "^4.1.10",
|
|
72
|
+
"zod": "^4.0.0"
|
|
73
|
+
},
|
|
74
|
+
"license": "MIT",
|
|
75
|
+
"scripts": {
|
|
76
|
+
"build": "tsup && tsc -p tsconfig.build.json",
|
|
77
|
+
"dev": "tsup --watch",
|
|
78
|
+
"typecheck": "tsc --noEmit",
|
|
79
|
+
"test": "vitest run",
|
|
80
|
+
"test:watch": "vitest",
|
|
81
|
+
"clean": "rm -rf dist"
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: percent-core
|
|
3
|
+
description: >
|
|
4
|
+
@eristack/percent ratio strings, basis points, percentOf/plus/minus for tax and
|
|
5
|
+
discounts without float literals. Use before @eristack/money rounding at boundaries.
|
|
6
|
+
metadata:
|
|
7
|
+
author: eristack
|
|
8
|
+
version: "0.1"
|
|
9
|
+
sources:
|
|
10
|
+
- packages/primitive/percent/docs/index.md
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
# @eristack/percent
|
|
14
|
+
|
|
15
|
+
Rates as **decimal ratio strings** — `"0.11"` for 11%, basis points `"1100"`.
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { parsePercent, percentOf, fromBasisPoints } from "@eristack/percent";
|
|
19
|
+
|
|
20
|
+
const vat = parsePercent("10%");
|
|
21
|
+
percentOf("100", vat); // "10"
|
|
22
|
+
fromBasisPoints("1100"); // 11%
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Checklist
|
|
26
|
+
|
|
27
|
+
1. Master data: store bps or ratio string — parse with `parsePercent` / `fromBasisPoints`.
|
|
28
|
+
2. Line math on strings; wrap in `@eristack/money` + round at invoice/ledger boundary.
|
|
29
|
+
3. Do not confuse with money `percentOf(m, "7")` (7 percent points) — convert ratio explicitly.
|
|
30
|
+
4. Compound tax/discount stacks are **app rules** — `addPercents` is simple sum only.
|
|
31
|
+
5. Validate wire JSON with `@eristack/percent/zod`; human `"11%"` input via `parsePercent`.
|
|
32
|
+
|
|
33
|
+
## Do not
|
|
34
|
+
|
|
35
|
+
- Pass JS float literals (`0.11`) as domain rates
|
|
36
|
+
- Assume statutory compound tax from `addPercents` alone
|