Helpers comunes

This commit is contained in:
David Arranz 2025-11-12 18:17:28 +01:00
parent 5f08cfaa15
commit 60dacc4c32
6 changed files with 194 additions and 27 deletions

View File

@ -0,0 +1,58 @@
type DateInput = string | number | Date;
/**
* Formatea una fecha según `locale` usando Intl.DateTimeFormat.
*
* - Acepta Date, timestamp o string.
* - Strings "date-only" (YYYY-MM-DD) se crean en hora local para evitar
* desfases por zona horaria (off-by-one).
* - Devuelve "" si la fecha es inválida.
*/
function formatDate(
input: DateInput,
locale?: string,
options?: Intl.DateTimeFormatOptions
): string {
const date = normalizeToDate(input);
if (!date || isNaN(date.getTime())) return "";
// Por defecto, formato corto y consistente.
const fmt = new Intl.DateTimeFormat(locale, {
year: "numeric",
month: "2-digit",
day: "2-digit",
...options,
});
return fmt.format(date);
}
/** Normaliza distintos inputs a Date. Maneja "YYYY-MM-DD" de forma segura. */
function normalizeToDate(input: DateInput): Date | null {
if (input instanceof Date) return input;
if (typeof input === "number") return new Date(input);
if (typeof input === "string") {
const trimmed = input.trim();
if (!trimmed) return null;
// Patrón "date-only" ISO (sin hora)
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(trimmed);
if (m) {
const [, y, mo, d] = m;
// new Date(y, m-1, d) crea la fecha en hora local -> evitamos shift
return new Date(Number(y), Number(mo) - 1, Number(d));
}
// Cualquier otra cadena se delega al parser nativo
const t = Date.parse(trimmed);
if (Number.isNaN(t)) return null;
return new Date(t);
}
return null;
}
export const DateHelper = {
format: formatDate,
};

View File

@ -1,3 +1,4 @@
export * from "./date-helper";
export * from "./dto-compare-helper"; export * from "./dto-compare-helper";
export * from "./money-dto-helper"; export * from "./money-dto-helper";
export * from "./money-helper"; export * from "./money-helper";

View File

@ -1,5 +1,6 @@
import Dinero, { Currency } from "dinero.js"; import Dinero, { type Currency } from "dinero.js";
import { MoneyDTO } from "../dto";
import type { MoneyDTO } from "../dto";
type DineroPlain = { amount: number; precision: number; currency: string }; type DineroPlain = { amount: number; precision: number; currency: string };
@ -28,10 +29,54 @@ const toNumericString = (dto?: MoneyDTO | null, fallbackScale = 2): string => {
return toNumber(dto, fallbackScale).toString(); return toNumber(dto, fallbackScale).toString();
}; };
/**
* Formatea un MoneyDTO según un locale.
*
* @param dto - DTO de porcentaje (p.ej. value="1250", scale="2" representa 12.50).
* @param locale - Locale BCP-47 (p.ej. "es-ES", "en-US"). Si no se pasa, usa el del runtime.
* @param options - Opciones de `Intl.NumberFormat`. Si no se indican `minimumFractionDigits`/`maximumFractionDigits`,
* se infieren del `scale` del DTO. Puedes sobreespecificarlas aquí.
* @param fallbackScale - Escala a usar si el DTO no la trae (por defecto 2).
* @returns Cadena formateada (p.ej. "12,50 €") o `""` si el DTO está vacío.
*
*/
const format = (
dto: MoneyDTO,
locale?: string,
options: { hideZeros?: boolean } & Intl.NumberFormatOptions = { hideZeros: false },
fallbackScale = 2
): string => {
const normalizedDTO = normalizeDTO(dto);
if (isEmptyMoneyDTO(normalizedDTO)) {
return "";
}
const scale = Number(normalizedDTO?.scale ?? fallbackScale);
// Respetar fracciones si no vienen dadas en options.
const nfOptions: Intl.NumberFormatOptions = {
style: "currency",
currency: normalizedDTO.currency_code,
minimumFractionDigits: options?.minimumFractionDigits ?? scale,
maximumFractionDigits: options?.maximumFractionDigits ?? scale,
...options,
};
const absolute = toNumber(normalizedDTO, fallbackScale); // ej. 12.5
if (options.hideZeros && absolute === 0) {
return "";
}
return new Intl.NumberFormat(locale, nfOptions).format(absolute);
};
/** /**
* Convierte número a MoneyDTO. * Convierte número a MoneyDTO.
*/ */
const fromNumber = (amount: number, currency: string = "EUR", scale = 2): MoneyDTO => { const fromNumber = (amount: number, currency = "EUR", scale = 2): MoneyDTO => {
return { return {
value: String(Math.round(amount * 10 ** scale)), value: String(Math.round(amount * 10 ** scale)),
scale: String(scale), scale: String(scale),
@ -42,7 +87,7 @@ const fromNumber = (amount: number, currency: string = "EUR", scale = 2): MoneyD
/** /**
* Convierte cadena numérica a MoneyDTO. * Convierte cadena numérica a MoneyDTO.
*/ */
const fromNumericString = (amount?: string, currency: string = "EUR", scale = 2): MoneyDTO => { const fromNumericString = (amount?: string, currency = "EUR", scale = 2): MoneyDTO => {
if (!amount || amount?.trim?.() === "") { if (!amount || amount?.trim?.() === "") {
return { return {
value: "", value: "",
@ -67,22 +112,13 @@ const normalizeDTO = (dto: MoneyDTO, fallbackCurrency = "EUR"): Required<MoneyDT
return { value: v, scale: s, currency_code: c }; return { value: v, scale: s, currency_code: c };
}; };
/**
* Formatea un MoneyDTO según locale.
*/
const formatDTO = (dto: MoneyDTO, locale: string = "es-ES"): string => {
const { value, scale, currency_code } = normalizeDTO(dto);
const num = Number(value) / 10 ** Number(scale);
return new Intl.NumberFormat(locale, { style: "currency", currency: currency_code }).format(num);
};
export const MoneyDTOHelper = { export const MoneyDTOHelper = {
isEmpty: isEmptyMoneyDTO, isEmpty: isEmptyMoneyDTO,
toNumber, toNumber,
toNumericString, toNumericString,
fromNumber, fromNumber,
fromNumericString, fromNumericString,
formatDTO, format,
}; };
/** /**

View File

@ -2,12 +2,7 @@
* Funciones para manipular valores monetarios numéricos. * Funciones para manipular valores monetarios numéricos.
*/ */
export const formatCurrency = ( export const formatCurrency = (amount: number, scale = 2, currency = "EUR", locale = "es-ES") => {
amount: number,
scale: number = 2,
currency = "EUR",
locale = "es-ES"
) => {
return new Intl.NumberFormat(locale, { return new Intl.NumberFormat(locale, {
style: "currency", style: "currency",
currency, currency,
@ -25,7 +20,7 @@ export const formatCurrency = (
*/ */
export const stripCurrencySymbols = (s: string): string => export const stripCurrencySymbols = (s: string): string =>
s s
.replace(/[^\d.,\-]/g, "") .replace(/[^\d.,-]/g, "")
.replace(/\s+/g, " ") .replace(/\s+/g, " ")
.trim(); .trim();

View File

@ -1,10 +1,10 @@
import { PercentageDTO } from "../dto"; import type { PercentageDTO } from "../dto";
const isEmptyPercentageDTO = (dto?: PercentageDTO | null) => const isEmptyPercentageDTO = (dto?: PercentageDTO | null) =>
!dto || dto.value?.trim?.() === "" || dto.scale?.trim?.() === ""; !dto || dto.value?.trim?.() === "" || dto.scale?.trim?.() === "";
/** /**
* Convierte un QuantityDTO a número con precisión. * Convierte un PercentageDTO a número con precisión.
*/ */
const toNumber = (dto?: PercentageDTO | null, fallbackScale = 2): number => { const toNumber = (dto?: PercentageDTO | null, fallbackScale = 2): number => {
if (isEmptyPercentageDTO(dto)) { if (isEmptyPercentageDTO(dto)) {
@ -15,7 +15,7 @@ const toNumber = (dto?: PercentageDTO | null, fallbackScale = 2): number => {
}; };
/** /**
* Convierte un QuantityDTO a cadena numérica con precisión. * Convierte un PercentageDTO a cadena numérica con precisión.
* Puede devolver cadena vacía * Puede devolver cadena vacía
*/ */
const toNumericString = (dto?: PercentageDTO | null, fallbackScale = 2): string => { const toNumericString = (dto?: PercentageDTO | null, fallbackScale = 2): string => {
@ -26,7 +26,46 @@ const toNumericString = (dto?: PercentageDTO | null, fallbackScale = 2): string
}; };
/** /**
* Convierte número a QuantityDTO. * Formatea un PercentageDTO según un locale.
*
* @param dto - DTO de porcentaje (p.ej. value="1250", scale="2" representa 12.50).
* @param locale - Locale BCP-47 (p.ej. "es-ES", "en-US"). Si no se pasa, usa el del runtime.
* @param options - Opciones de `Intl.NumberFormat`. Si no se indican `minimumFractionDigits`/`maximumFractionDigits`,
* se infieren del `scale` del DTO. Puedes sobreespecificarlas aquí.
* @param fallbackScale - Escala a usar si el DTO no la trae (por defecto 2).
* @returns Cadena formateada (p.ej. "12,50 %") o `""` si el DTO está vacío.
*
* Nota: `Intl.NumberFormat` con `style:"percent"` espera una fracción (0.125 "12,5 %").
* Este helper convierte el valor absoluto del DTO (12.5) a fracción dividiendo entre 100.
*/
const format = (
dto: PercentageDTO,
locale?: string,
options?: Intl.NumberFormatOptions,
fallbackScale = 2
): string => {
if (isEmptyPercentageDTO(dto)) {
return "";
}
const scale = Number(dto?.scale ?? fallbackScale);
// Respetar fracciones si no vienen dadas en options.
const nfOptions: Intl.NumberFormatOptions = {
style: "percent",
minimumFractionDigits: options?.minimumFractionDigits ?? scale,
maximumFractionDigits: options?.maximumFractionDigits ?? scale,
...options,
};
const absolute = toNumber(dto, fallbackScale); // ej. 12.5
const fraction = absolute / 100; // ej. 0.125 para Intl percent
return new Intl.NumberFormat(locale, nfOptions).format(fraction);
};
/**
* Convierte número a PercentageDTO.
*/ */
const fromNumber = (amount: number, scale = 2): PercentageDTO => { const fromNumber = (amount: number, scale = 2): PercentageDTO => {
return { return {
@ -36,7 +75,7 @@ const fromNumber = (amount: number, scale = 2): PercentageDTO => {
}; };
/** /**
* Convierte cadena numérica a QuantityDTO. * Convierte cadena numérica a PercentageDTO.
*/ */
const fromNumericString = (amount?: string, scale = 2): PercentageDTO => { const fromNumericString = (amount?: string, scale = 2): PercentageDTO => {
if (!amount || amount?.trim?.() === "") { if (!amount || amount?.trim?.() === "") {
@ -57,4 +96,5 @@ export const PercentageDTOHelper = {
toNumericString, toNumericString,
fromNumber, fromNumber,
fromNumericString, fromNumericString,
format,
}; };

View File

@ -1,4 +1,4 @@
import { QuantityDTO } from "../dto"; import type { QuantityDTO } from "../dto";
const isEmptyQuantityDTO = (dto?: QuantityDTO | null) => const isEmptyQuantityDTO = (dto?: QuantityDTO | null) =>
!dto || dto.value?.trim?.() === "" || dto.scale?.trim?.() === ""; !dto || dto.value?.trim?.() === "" || dto.scale?.trim?.() === "";
@ -25,6 +25,42 @@ const toNumericString = (dto?: QuantityDTO | null, fallbackScale = 2): string =>
return toNumber(dto, fallbackScale).toString(); return toNumber(dto, fallbackScale).toString();
}; };
/**
* Formatea un QuantityDTO según un locale.
*
* @param dto - DTO de porcentaje (p.ej. value="1250", scale="2" representa 12.50).
* @param locale - Locale BCP-47 (p.ej. "es-ES", "en-US"). Si no se pasa, usa el del runtime.
* @param options - Opciones de `Intl.NumberFormat`. Si no se indican `minimumFractionDigits`/`maximumFractionDigits`,
* se infieren del `scale` del DTO. Puedes sobreespecificarlas aquí.
* @param fallbackScale - Escala a usar si el DTO no la trae (por defecto 2).
* @returns Cadena formateada (p.ej. "12,50") o `""` si el DTO está vacío.
*
*/
const format = (
dto: QuantityDTO,
locale?: string,
options?: Intl.NumberFormatOptions,
fallbackScale = 0
): string => {
if (isEmptyQuantityDTO(dto)) {
return "";
}
const scale = Number(dto?.scale ?? fallbackScale);
// Respetar fracciones si no vienen dadas en options.
const nfOptions: Intl.NumberFormatOptions = {
style: "decimal",
minimumFractionDigits: options?.minimumFractionDigits ?? scale,
maximumFractionDigits: options?.maximumFractionDigits ?? scale,
...options,
};
const absolute = toNumber(dto, fallbackScale); // ej. 12.5
return new Intl.NumberFormat(locale, nfOptions).format(absolute);
};
/** /**
* Convierte número a QuantityDTO. * Convierte número a QuantityDTO.
*/ */
@ -57,4 +93,5 @@ export const QuantityDTOHelper = {
toNumericString, toNumericString,
fromNumber, fromNumber,
fromNumericString, fromNumericString,
format,
}; };