@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 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"]}
@@ -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,2 @@
1
+ export { percentRatioSchema, percentSchema, type PercentJson, } from "./schemas.js";
2
+ //# sourceMappingURL=index.d.ts.map
@@ -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,11 @@
1
+ // src/zod/schemas.ts
2
+ import { z } from "zod";
3
+ var percentRatioSchema = z.string().min(1);
4
+ var percentSchema = z.object({
5
+ ratio: percentRatioSchema
6
+ });
7
+ export {
8
+ percentRatioSchema,
9
+ percentSchema
10
+ };
11
+ //# sourceMappingURL=index.js.map
@@ -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"}
@@ -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.
@@ -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
@@ -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
@@ -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