Заведи каркас пакета и утилитный слой MCP-сервера
package.json, tsconfig, eslint, генерация типов из OpenAPI-спеки, модули config/redact/errors/money/sanitize/cache с тестами и обезличенные фикстуры живых ответов API openrusrouter.ru. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
29 files changed
+11350
No files matched your search
+117
@@ -0,0 +1,117 @@
|
||||
// Денежная арифметика без float. API отдаёт суммы decimal-строками
|
||||
// (shopspring/decimal на бэкенде); здесь они живут как целое × 10^-scale.
|
||||
// number для денег не используется нигде — ни на входе, ни внутри.
|
||||
|
||||
export interface Decimal {
|
||||
readonly int: bigint;
|
||||
readonly scale: number;
|
||||
}
|
||||
|
||||
const DECIMAL_RE = /^(-?)(\d+)(?:\.(\d+))?$/;
|
||||
|
||||
function assertScale(scale: number): void {
|
||||
if (!Number.isSafeInteger(scale) || scale < 0) {
|
||||
throw new RangeError(`недопустимый scale: ${String(scale)}`);
|
||||
}
|
||||
}
|
||||
|
||||
function pow10(n: number): bigint {
|
||||
assertScale(n);
|
||||
return 10n ** BigInt(n);
|
||||
}
|
||||
|
||||
function make(int: bigint, scale: number): Decimal {
|
||||
assertScale(scale);
|
||||
return Object.freeze({ int, scale });
|
||||
}
|
||||
|
||||
/** Строгий разбор decimal-строки: без экспоненты, без запятой, без пробелов, только string. */
|
||||
export function parseDecimal(s: unknown): Decimal {
|
||||
if (typeof s !== "string") {
|
||||
throw new TypeError(`ожидалась decimal-строка, получено ${typeof s}`);
|
||||
}
|
||||
const m = DECIMAL_RE.exec(s);
|
||||
if (m === null) {
|
||||
throw new SyntaxError(`некорректная decimal-строка: ${JSON.stringify(s.slice(0, 40))}`);
|
||||
}
|
||||
const [, sign = "", whole = "", frac = ""] = m;
|
||||
const int = BigInt(whole + frac);
|
||||
return make(sign === "-" ? -int : int, frac.length);
|
||||
}
|
||||
|
||||
export function fromBigInt(n: bigint): Decimal {
|
||||
return make(n, 0);
|
||||
}
|
||||
|
||||
function rescale(a: Decimal, scale: number): bigint {
|
||||
// Только повышение точности — без потерь.
|
||||
return a.int * pow10(scale - a.scale);
|
||||
}
|
||||
|
||||
export function add(a: Decimal, b: Decimal): Decimal {
|
||||
const scale = Math.max(a.scale, b.scale);
|
||||
return make(rescale(a, scale) + rescale(b, scale), scale);
|
||||
}
|
||||
|
||||
export function mul(a: Decimal, b: Decimal): Decimal {
|
||||
return make(a.int * b.int, a.scale + b.scale);
|
||||
}
|
||||
|
||||
export function mulByBigInt(a: Decimal, n: bigint): Decimal {
|
||||
return make(a.int * n, a.scale);
|
||||
}
|
||||
|
||||
/** Деление на 10^p — точное, просто сдвиг scale. */
|
||||
export function divPow10(a: Decimal, p: number): Decimal {
|
||||
assertScale(p);
|
||||
return make(a.int, a.scale + p);
|
||||
}
|
||||
|
||||
/** ROUND HALF UP (половина — от нуля), как decimal.Round в shopspring. */
|
||||
export function round(a: Decimal, scale: number): Decimal {
|
||||
assertScale(scale);
|
||||
if (a.scale <= scale) {
|
||||
return make(rescale(a, scale), scale);
|
||||
}
|
||||
const divisor = pow10(a.scale - scale);
|
||||
const negative = a.int < 0n;
|
||||
const abs = negative ? -a.int : a.int;
|
||||
let q = abs / divisor;
|
||||
const r = abs % divisor;
|
||||
if (r * 2n >= divisor) {
|
||||
q += 1n;
|
||||
}
|
||||
return make(negative ? -q : q, scale);
|
||||
}
|
||||
|
||||
export function compare(a: Decimal, b: Decimal): -1 | 0 | 1 {
|
||||
const scale = Math.max(a.scale, b.scale);
|
||||
const x = rescale(a, scale);
|
||||
const y = rescale(b, scale);
|
||||
return x < y ? -1 : x > y ? 1 : 0;
|
||||
}
|
||||
|
||||
function format(int: bigint, scale: number): string {
|
||||
const negative = int < 0n;
|
||||
const digits = (negative ? -int : int).toString().padStart(scale + 1, "0");
|
||||
const whole = digits.slice(0, digits.length - scale);
|
||||
const frac = digits.slice(digits.length - scale);
|
||||
const body = scale > 0 ? `${whole}.${frac}` : whole;
|
||||
return negative ? `-${body}` : body;
|
||||
}
|
||||
|
||||
/** Канонический вид: без незначащих нулей справа; "0.00" и "-0" дают "0". */
|
||||
export function toString(a: Decimal): string {
|
||||
let { int, scale } = a;
|
||||
while (scale > 0 && int % 10n === 0n) {
|
||||
int /= 10n;
|
||||
scale -= 1;
|
||||
}
|
||||
return format(int, scale);
|
||||
}
|
||||
|
||||
/** Ровно scale знаков после точки, с округлением HALF UP. */
|
||||
export function toFixed(a: Decimal, scale: number): string {
|
||||
const r = round(a, scale);
|
||||
return format(r.int, r.scale);
|
||||
}
|
||||
Reference in new issue
Block a user