57 lines
1.6 KiB
TypeScript
57 lines
1.6 KiB
TypeScript
export const MYSQL_UNSIGNED_INT_MAX = 4_294_967_295;
|
|
|
|
export interface LegacyMoneyOptions {
|
|
nullable?: boolean;
|
|
maxCents?: number;
|
|
field?: string;
|
|
}
|
|
|
|
function describeField(field: string | undefined): string {
|
|
return field ? ` for ${field}` : '';
|
|
}
|
|
|
|
export function legacyDecimalToCents(
|
|
value: unknown,
|
|
options: LegacyMoneyOptions = {}
|
|
): number | null {
|
|
const field = describeField(options.field);
|
|
|
|
if (value === null || value === undefined) {
|
|
if (options.nullable) {
|
|
return null;
|
|
}
|
|
throw new Error(`Legacy money value${field} cannot be null.`);
|
|
}
|
|
|
|
if (typeof value !== 'string' && typeof value !== 'number') {
|
|
throw new Error(`Legacy money value${field} must be a decimal string or number.`);
|
|
}
|
|
|
|
if (typeof value === 'number' && !Number.isFinite(value)) {
|
|
throw new Error(`Legacy money value${field} must be finite.`);
|
|
}
|
|
|
|
const decimal = String(value);
|
|
const match = /^(\d+)(?:\.(\d{1,2}))?$/.exec(decimal);
|
|
if (!match) {
|
|
throw new Error(
|
|
`Legacy money value${field} must be a non-negative decimal with at most two fractional digits.`
|
|
);
|
|
}
|
|
|
|
const wholeCents = BigInt(match[1]) * 100n;
|
|
const fraction = (match[2] ?? '').padEnd(2, '0');
|
|
const cents = wholeCents + BigInt(fraction || '0');
|
|
const maxCents = options.maxCents ?? MYSQL_UNSIGNED_INT_MAX;
|
|
|
|
if (!Number.isSafeInteger(maxCents) || maxCents < 0) {
|
|
throw new Error('Legacy money maxCents must be a non-negative safe integer.');
|
|
}
|
|
|
|
if (cents > BigInt(maxCents)) {
|
|
throw new Error(`Legacy money value${field} exceeds the target cents column limit.`);
|
|
}
|
|
|
|
return Number(cents);
|
|
}
|