From 60dacc4c32075a4063cf882e6c2ae46674355874 Mon Sep 17 00:00:00 2001 From: david Date: Wed, 12 Nov 2025 18:17:28 +0100 Subject: [PATCH] Helpers comunes --- .../core/src/common/helpers/date-helper.ts | 58 +++++++++++++++++ modules/core/src/common/helpers/index.ts | 1 + .../src/common/helpers/money-dto-helper.ts | 64 +++++++++++++++---- .../core/src/common/helpers/money-helper.ts | 9 +-- .../common/helpers/percentage-dto-helpers.ts | 50 +++++++++++++-- .../common/helpers/quantity-dto-helpers.ts | 39 ++++++++++- 6 files changed, 194 insertions(+), 27 deletions(-) create mode 100644 modules/core/src/common/helpers/date-helper.ts diff --git a/modules/core/src/common/helpers/date-helper.ts b/modules/core/src/common/helpers/date-helper.ts new file mode 100644 index 00000000..cb3b51b7 --- /dev/null +++ b/modules/core/src/common/helpers/date-helper.ts @@ -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, +}; diff --git a/modules/core/src/common/helpers/index.ts b/modules/core/src/common/helpers/index.ts index 55b76df1..5450b54c 100644 --- a/modules/core/src/common/helpers/index.ts +++ b/modules/core/src/common/helpers/index.ts @@ -1,3 +1,4 @@ +export * from "./date-helper"; export * from "./dto-compare-helper"; export * from "./money-dto-helper"; export * from "./money-helper"; diff --git a/modules/core/src/common/helpers/money-dto-helper.ts b/modules/core/src/common/helpers/money-dto-helper.ts index 9f6d3608..9c01f815 100644 --- a/modules/core/src/common/helpers/money-dto-helper.ts +++ b/modules/core/src/common/helpers/money-dto-helper.ts @@ -1,5 +1,6 @@ -import Dinero, { Currency } from "dinero.js"; -import { MoneyDTO } from "../dto"; +import Dinero, { type Currency } from "dinero.js"; + +import type { MoneyDTO } from "../dto"; 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(); }; +/** + * 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. */ -const fromNumber = (amount: number, currency: string = "EUR", scale = 2): MoneyDTO => { +const fromNumber = (amount: number, currency = "EUR", scale = 2): MoneyDTO => { return { value: String(Math.round(amount * 10 ** scale)), scale: String(scale), @@ -42,7 +87,7 @@ const fromNumber = (amount: number, currency: string = "EUR", scale = 2): MoneyD /** * 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?.() === "") { return { value: "", @@ -67,22 +112,13 @@ const normalizeDTO = (dto: MoneyDTO, fallbackCurrency = "EUR"): Required { - 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 = { isEmpty: isEmptyMoneyDTO, toNumber, toNumericString, fromNumber, fromNumericString, - formatDTO, + format, }; /** diff --git a/modules/core/src/common/helpers/money-helper.ts b/modules/core/src/common/helpers/money-helper.ts index b7064b4c..c2099eab 100644 --- a/modules/core/src/common/helpers/money-helper.ts +++ b/modules/core/src/common/helpers/money-helper.ts @@ -2,12 +2,7 @@ * Funciones para manipular valores monetarios numéricos. */ -export const formatCurrency = ( - amount: number, - scale: number = 2, - currency = "EUR", - locale = "es-ES" -) => { +export const formatCurrency = (amount: number, scale = 2, currency = "EUR", locale = "es-ES") => { return new Intl.NumberFormat(locale, { style: "currency", currency, @@ -25,7 +20,7 @@ export const formatCurrency = ( */ export const stripCurrencySymbols = (s: string): string => s - .replace(/[^\d.,\-]/g, "") + .replace(/[^\d.,-]/g, "") .replace(/\s+/g, " ") .trim(); diff --git a/modules/core/src/common/helpers/percentage-dto-helpers.ts b/modules/core/src/common/helpers/percentage-dto-helpers.ts index 7f02028b..37c5e8b4 100644 --- a/modules/core/src/common/helpers/percentage-dto-helpers.ts +++ b/modules/core/src/common/helpers/percentage-dto-helpers.ts @@ -1,10 +1,10 @@ -import { PercentageDTO } from "../dto"; +import type { PercentageDTO } from "../dto"; const isEmptyPercentageDTO = (dto?: PercentageDTO | null) => !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 => { 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 */ 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 => { 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 => { if (!amount || amount?.trim?.() === "") { @@ -57,4 +96,5 @@ export const PercentageDTOHelper = { toNumericString, fromNumber, fromNumericString, + format, }; diff --git a/modules/core/src/common/helpers/quantity-dto-helpers.ts b/modules/core/src/common/helpers/quantity-dto-helpers.ts index 92470fb9..c82aaa5e 100644 --- a/modules/core/src/common/helpers/quantity-dto-helpers.ts +++ b/modules/core/src/common/helpers/quantity-dto-helpers.ts @@ -1,4 +1,4 @@ -import { QuantityDTO } from "../dto"; +import type { QuantityDTO } from "../dto"; const isEmptyQuantityDTO = (dto?: QuantityDTO | null) => !dto || dto.value?.trim?.() === "" || dto.scale?.trim?.() === ""; @@ -25,6 +25,42 @@ const toNumericString = (dto?: QuantityDTO | null, fallbackScale = 2): string => 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. */ @@ -57,4 +93,5 @@ export const QuantityDTOHelper = { toNumericString, fromNumber, fromNumericString, + format, };