@leaderty/utils 2.0.1 → 2.1.0

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/README.md ADDED
@@ -0,0 +1,114 @@
1
+ # @leaderty/utils
2
+
3
+ Argentine number and date formatting. Every function returns the same output on any system: none of them uses `Intl`, `toLocaleString()` or the system time zone, so a WebView in English still shows `350.000` and `dd/mm/aaaa`.
4
+
5
+ ## Setup
6
+
7
+ ```bash
8
+ npm install @leaderty/utils
9
+ ```
10
+
11
+ ## Functions
12
+
13
+ ### `numberToMoney(amount, options?)`
14
+
15
+ Formats a money amount with `.` for thousands and `,` for decimals, optionally prefixed with the currency.
16
+
17
+ **Behavior:**
18
+ - Rounds half away from zero on the decimal value: `1.005` gives `1,01`, and float noise such as `2309.9999999999995` gives `2.310,00`.
19
+ - Puts the minus sign before the currency (`-$ 1.234,50`). An amount that rounds to zero has no sign.
20
+ - Returns `'-'` for `null`, `undefined`, `NaN` and infinite amounts.
21
+
22
+ **Parameters:**
23
+ - `amount` - `number | null | undefined`
24
+ - `options.decimals` - `0 | 2`. Defaults to `0`.
25
+ - `options.currency` - `'ARS' | 'USD'`. Omit it for a bare number.
26
+ - `options.currencyDisplay` - `'code'` (default) prints `ARS` / `USD`; `'symbol'` prints `$` / `US$`. Ignored without `currency`.
27
+
28
+ **Returns:** `string`
29
+
30
+ **Examples:**
31
+ ```typescript
32
+ numberToMoney(350000) // "350.000"
33
+ numberToMoney(350000, { currency: 'ARS' }) // "ARS 350.000"
34
+ numberToMoney(350000, { currency: 'ARS', currencyDisplay: 'symbol' }) // "$ 350.000"
35
+ numberToMoney(350000, { currency: 'ARS', currencyDisplay: 'symbol', decimals: 2 }) // "$ 350.000,00"
36
+ numberToMoney(1500, { currency: 'USD', currencyDisplay: 'symbol', decimals: 2 }) // "US$ 1.500,00"
37
+ numberToMoney(-1234.5, { decimals: 2 }) // "-1.234,50"
38
+ numberToMoney(undefined, { currency: 'ARS' }) // "-"
39
+ ```
40
+
41
+ ### `dateToShort(value, options?)`
42
+
43
+ Formats a date as `dd/mm/aaaa`, or `dd/mm/aaaa hh:mm` (24-hour) with `time: true`.
44
+
45
+ **Behavior:**
46
+ - `'aaaa-mm-dd'` strings, and date-time strings without a time zone (`'2026-10-01T00:00:00'`), are read as written. `'2026-10-11'` gives `11/10/2026`, even though `new Date('2026-10-11')` is midnight UTC, which is still October 10 in Argentina.
47
+ - `Date` objects and ISO strings with a time zone (`Z` or `±hh:mm`, as in `'2026-10-22T22:05:36.036Z'`) are shown in `America/Argentina/Buenos_Aires` time (UTC-3, no daylight saving time).
48
+ - Any other string gives `'-'` (`'11/10/2026'`, `'2026-10'`, `'Oct 11 2026'`): `new Date()` would read it in the system time zone or guess the day/month order.
49
+ - Also returns `'-'` for `null`, `undefined`, invalid dates and impossible dates such as `'2026-02-30'`.
50
+
51
+ **Parameters:**
52
+ - `value` - `Date | string | null | undefined`
53
+ - `options.time` - Appends the time. Defaults to `false`.
54
+
55
+ **Returns:** `string`
56
+
57
+ **Examples:**
58
+ ```typescript
59
+ dateToShort('2026-10-11') // "11/10/2026"
60
+ dateToShort('2026-10-01T00:00:00') // "01/10/2026"
61
+ dateToShort(new Date('2026-10-22T22:05:36Z')) // "22/10/2026"
62
+ dateToShort('2026-10-22T02:05:36Z', { time: true }) // "21/10/2026 23:05"
63
+ dateToShort('11/10/2026') // "-"
64
+ ```
65
+
66
+ ### `dateToYMD(value)`
67
+
68
+ Formats a date as `aaaa-mm-dd`, the value of an `<input type="date">`. Reads `value` like `dateToShort`, so `'2026-10-11'` stays the 11th and a `Date` is taken in Buenos Aires time.
69
+
70
+ **Parameters:**
71
+ - `value` - `Date | string | null | undefined`
72
+
73
+ **Returns:** `string`. `''` (not `'-'`, so a date input stays empty) when `value` is missing or not a valid date.
74
+
75
+ **Examples:**
76
+ ```typescript
77
+ dateToYMD('2026-10-11') // "2026-10-11"
78
+ dateToYMD(new Date('2026-10-22T02:05:36Z')) // "2026-10-21"
79
+ dateToYMD('11/10/2026') // ""
80
+ ```
81
+
82
+ ### `dateToMonthYearLong(value)`
83
+
84
+ Formats the month and year in Spanish, lowercase. Also accepts `'aaaa-mm'` strings (the `MonthSelector` value of `@leaderty/react-month-selector`); otherwise reads `value` like `dateToShort`.
85
+
86
+ **Parameters:**
87
+ - `value` - `Date | string | null | undefined`
88
+
89
+ **Returns:** `string`, or `'-'` when the date is missing or invalid
90
+
91
+ **Examples:**
92
+ ```typescript
93
+ dateToMonthYearLong('2026-10') // "octubre de 2026"
94
+ dateToMonthYearLong('2026-10-11') // "octubre de 2026"
95
+ dateToMonthYearLong('2026-10-01') // "octubre de 2026" (not September)
96
+ ```
97
+
98
+ ### `numberToDecimalString(numero)`
99
+
100
+ Formats a number with two decimals and thousands separators. Rounds with `toFixed(2)`, so `1.005` gives `1,00`; prefer `numberToMoney(n, { decimals: 2 })` in new code. Formerly `numberToStringWithDecimals`, which stays as a deprecated alias.
101
+
102
+ **Parameters:**
103
+ - `numero` - `number`
104
+
105
+ **Returns:** `string`
106
+
107
+ **Examples:**
108
+ ```typescript
109
+ numberToDecimalString(1234.56) // "1.234,56"
110
+ ```
111
+
112
+ ## Types
113
+
114
+ `Currency`, `NumberToMoneyOptions` and `DateToShortOptions` are exported for consumers.
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Argentine display formatters that never depend on the system locale or `Intl`:
3
+ * the WebView formats `toLocaleString()` with the OS language, so an English
4
+ * system shows "350,000" where an Argentine user reads 350 pesos.
5
+ */
6
+ export type Currency = 'ARS' | 'USD';
7
+ export interface NumberToMoneyOptions {
8
+ /** Decimal places. Defaults to `0`. */
9
+ decimals?: 0 | 2;
10
+ /** Prefixes the amount with the currency. Omit it for a bare number. */
11
+ currency?: Currency;
12
+ /** `'code'` (default) prints `ARS` / `USD`; `'symbol'` prints `$` / `US$`. Ignored without `currency`. */
13
+ currencyDisplay?: 'code' | 'symbol';
14
+ }
15
+ export interface DateToShortOptions {
16
+ /** Appends the 24-hour time as `hh:mm`. Defaults to `false`. */
17
+ time?: boolean;
18
+ }
19
+ /**
20
+ * Formats a money amount the Argentine way: `.` for thousands and `,` for decimals.
21
+ * Rounds half away from zero on the decimal value, so `1.005` gives `1,01` and float noise
22
+ * such as `2309.9999999999995` gives `2.310,00`.
23
+ *
24
+ * @param amount - Amount to format
25
+ * @param options - Decimals and currency prefix
26
+ * @returns The formatted amount, or `'-'` when `amount` is `null`, `undefined`, `NaN` or infinite
27
+ *
28
+ * @example
29
+ * numberToMoney(350000) // "350.000"
30
+ * numberToMoney(350000, { currency: 'ARS' }) // "ARS 350.000"
31
+ * numberToMoney(350000, { currency: 'ARS', currencyDisplay: 'symbol', decimals: 2 }) // "$ 350.000,00"
32
+ * numberToMoney(-1234.5, { currency: 'USD', currencyDisplay: 'symbol', decimals: 2 }) // "-US$ 1.234,50"
33
+ */
34
+ export declare function numberToMoney(amount: number | null | undefined, options?: NumberToMoneyOptions): string;
35
+ /**
36
+ * Formats a date as `dd/mm/aaaa`, optionally with the time as `dd/mm/aaaa hh:mm`.
37
+ *
38
+ * - `'aaaa-mm-dd'` strings, and date-time strings without a time zone (`'2026-10-01T00:00:00'`),
39
+ * are read as written: `'2026-10-11'` is the 11th, never the 10th
40
+ * (`new Date('2026-10-11')` is midnight UTC, which is still the 10th in Argentina).
41
+ * - `Date` objects and ISO strings with a time zone (`'2026-10-22T22:05:36.036Z'`) are shown in
42
+ * `America/Argentina/Buenos_Aires` time.
43
+ * - Any other string (`'11/10/2026'`, `'2026-10'`, `'Oct 11 2026'`) gives `'-'`: the engine would parse it
44
+ * in the system time zone, or guess the day/month order.
45
+ *
46
+ * @param value - Date to format
47
+ * @param options - Whether to append the time
48
+ * @returns The formatted date, or `'-'` when `value` is missing or not a valid date
49
+ *
50
+ * @example
51
+ * dateToShort('2026-10-11') // "11/10/2026"
52
+ * dateToShort(new Date('2026-10-22T22:05:36Z')) // "22/10/2026"
53
+ * dateToShort('2026-10-22T02:05:36Z', { time: true }) // "21/10/2026 23:05"
54
+ */
55
+ export declare function dateToShort(value: Date | string | null | undefined, options?: DateToShortOptions): string;
56
+ /**
57
+ * Formats a date as `aaaa-mm-dd`, the value of an `<input type="date">`. Reads `value` like {@link dateToShort}.
58
+ *
59
+ * @param value - Date to format
60
+ * @returns The date as `aaaa-mm-dd`, or `''` (not `'-'`, so a date input stays empty) when `value` is missing or not a valid date
61
+ *
62
+ * @example
63
+ * dateToYMD('2026-10-11') // "2026-10-11"
64
+ * dateToYMD('2026-10-11T00:00:00') // "2026-10-11"
65
+ * dateToYMD(new Date('2026-10-22T02:05:36Z')) // "2026-10-21"
66
+ * dateToYMD('11/10/2026') // ""
67
+ */
68
+ export declare function dateToYMD(value: Date | string | null | undefined): string;
69
+ /**
70
+ * Formats the month and year of a date in Spanish, lowercase: `octubre de 2026`.
71
+ * Also accepts `'aaaa-mm'` strings (the `MonthSelector` value). Otherwise reads `value` like {@link dateToShort}.
72
+ *
73
+ * @param value - Date to format
74
+ * @returns The month and year, or `'-'` when `value` is missing or not a valid date
75
+ *
76
+ * @example
77
+ * dateToMonthYearLong('2026-10') // "octubre de 2026"
78
+ * dateToMonthYearLong('2026-10-11') // "octubre de 2026"
79
+ * dateToMonthYearLong(new Date(2027, 0, 11)) // "enero de 2027"
80
+ */
81
+ export declare function dateToMonthYearLong(value: Date | string | null | undefined): string;
package/dist/format.js ADDED
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Argentine display formatters that never depend on the system locale or `Intl`:
3
+ * the WebView formats `toLocaleString()` with the OS language, so an English
4
+ * system shows "350,000" where an Argentine user reads 350 pesos.
5
+ */
6
+ /** Shown instead of a value that is missing or not a valid number/date. */
7
+ const PLACEHOLDER = '-';
8
+ const CURRENCY_SYMBOLS = { ARS: '$', USD: 'US$' };
9
+ /** Argentina is UTC-3 all year (no daylight saving time since 2009). */
10
+ const BUENOS_AIRES_OFFSET_MS = -3 * 60 * 60 * 1000;
11
+ /** `aaaa-mm-dd`, optionally followed by a time and no time zone: a calendar date or Argentine wall-clock time. */
12
+ const WALL_CLOCK_PATTERN = /^(\d{4})-(\d{2})-(\d{2})(?:[T ](\d{2}):(\d{2})(?::\d{2}(?:\.\d+)?)?)?$/;
13
+ /** ISO 8601 date-time with an explicit time zone (`Z` or `±hh:mm`): an instant, the same in any system time zone. */
14
+ const ZONED_PATTERN = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:\d{2})$/;
15
+ /** `aaaa-mm`, the month format of `@leaderty/react-month-selector` (same pattern as its `YEAR_MONTH_PATTERN`). */
16
+ const YEAR_MONTH_PATTERN = /^(\d{4})-(0[1-9]|1[0-2])$/;
17
+ const MONTH_NAMES = [
18
+ 'enero', 'febrero', 'marzo', 'abril', 'mayo', 'junio',
19
+ 'julio', 'agosto', 'septiembre', 'octubre', 'noviembre', 'diciembre',
20
+ ];
21
+ /**
22
+ * Formats a money amount the Argentine way: `.` for thousands and `,` for decimals.
23
+ * Rounds half away from zero on the decimal value, so `1.005` gives `1,01` and float noise
24
+ * such as `2309.9999999999995` gives `2.310,00`.
25
+ *
26
+ * @param amount - Amount to format
27
+ * @param options - Decimals and currency prefix
28
+ * @returns The formatted amount, or `'-'` when `amount` is `null`, `undefined`, `NaN` or infinite
29
+ *
30
+ * @example
31
+ * numberToMoney(350000) // "350.000"
32
+ * numberToMoney(350000, { currency: 'ARS' }) // "ARS 350.000"
33
+ * numberToMoney(350000, { currency: 'ARS', currencyDisplay: 'symbol', decimals: 2 }) // "$ 350.000,00"
34
+ * numberToMoney(-1234.5, { currency: 'USD', currencyDisplay: 'symbol', decimals: 2 }) // "-US$ 1.234,50"
35
+ */
36
+ export function numberToMoney(amount, options = {}) {
37
+ if (amount === null || amount === undefined || !Number.isFinite(amount))
38
+ return PLACEHOLDER;
39
+ const { decimals = 0, currency, currencyDisplay = 'code' } = options;
40
+ const absolute = Math.abs(amount);
41
+ // Shifting the exponent in the string avoids binary noise: 1.005 * 100 === 100.49999999999999
42
+ const shifted = String(absolute).includes('e')
43
+ ? absolute * 10 ** decimals
44
+ : Number(`${absolute}e${decimals}`);
45
+ const digits = String(Math.round(shifted)).padStart(decimals + 1, '0');
46
+ const integerPart = digits.slice(0, digits.length - decimals).replace(/\B(?=(\d{3})+(?!\d))/g, '.');
47
+ const decimalPart = digits.slice(digits.length - decimals);
48
+ const number = decimals > 0 ? `${integerPart},${decimalPart}` : integerPart;
49
+ // An amount that rounds to zero has no sign: -0.001 gives "0,00", not "-0,00"
50
+ const sign = amount < 0 && /[1-9]/.test(digits) ? '-' : '';
51
+ if (!currency)
52
+ return `${sign}${number}`;
53
+ const prefix = currencyDisplay === 'symbol' ? CURRENCY_SYMBOLS[currency] : currency;
54
+ return `${sign}${prefix} ${number}`;
55
+ }
56
+ /**
57
+ * Formats a date as `dd/mm/aaaa`, optionally with the time as `dd/mm/aaaa hh:mm`.
58
+ *
59
+ * - `'aaaa-mm-dd'` strings, and date-time strings without a time zone (`'2026-10-01T00:00:00'`),
60
+ * are read as written: `'2026-10-11'` is the 11th, never the 10th
61
+ * (`new Date('2026-10-11')` is midnight UTC, which is still the 10th in Argentina).
62
+ * - `Date` objects and ISO strings with a time zone (`'2026-10-22T22:05:36.036Z'`) are shown in
63
+ * `America/Argentina/Buenos_Aires` time.
64
+ * - Any other string (`'11/10/2026'`, `'2026-10'`, `'Oct 11 2026'`) gives `'-'`: the engine would parse it
65
+ * in the system time zone, or guess the day/month order.
66
+ *
67
+ * @param value - Date to format
68
+ * @param options - Whether to append the time
69
+ * @returns The formatted date, or `'-'` when `value` is missing or not a valid date
70
+ *
71
+ * @example
72
+ * dateToShort('2026-10-11') // "11/10/2026"
73
+ * dateToShort(new Date('2026-10-22T22:05:36Z')) // "22/10/2026"
74
+ * dateToShort('2026-10-22T02:05:36Z', { time: true }) // "21/10/2026 23:05"
75
+ */
76
+ export function dateToShort(value, options = {}) {
77
+ const parts = toDateParts(value);
78
+ if (!parts)
79
+ return PLACEHOLDER;
80
+ const date = `${pad(parts.day)}/${pad(parts.month)}/${parts.year}`;
81
+ return options.time ? `${date} ${pad(parts.hours)}:${pad(parts.minutes)}` : date;
82
+ }
83
+ /**
84
+ * Formats a date as `aaaa-mm-dd`, the value of an `<input type="date">`. Reads `value` like {@link dateToShort}.
85
+ *
86
+ * @param value - Date to format
87
+ * @returns The date as `aaaa-mm-dd`, or `''` (not `'-'`, so a date input stays empty) when `value` is missing or not a valid date
88
+ *
89
+ * @example
90
+ * dateToYMD('2026-10-11') // "2026-10-11"
91
+ * dateToYMD('2026-10-11T00:00:00') // "2026-10-11"
92
+ * dateToYMD(new Date('2026-10-22T02:05:36Z')) // "2026-10-21"
93
+ * dateToYMD('11/10/2026') // ""
94
+ */
95
+ export function dateToYMD(value) {
96
+ const parts = toDateParts(value);
97
+ if (!parts)
98
+ return '';
99
+ return `${parts.year}-${pad(parts.month)}-${pad(parts.day)}`;
100
+ }
101
+ /**
102
+ * Formats the month and year of a date in Spanish, lowercase: `octubre de 2026`.
103
+ * Also accepts `'aaaa-mm'` strings (the `MonthSelector` value). Otherwise reads `value` like {@link dateToShort}.
104
+ *
105
+ * @param value - Date to format
106
+ * @returns The month and year, or `'-'` when `value` is missing or not a valid date
107
+ *
108
+ * @example
109
+ * dateToMonthYearLong('2026-10') // "octubre de 2026"
110
+ * dateToMonthYearLong('2026-10-11') // "octubre de 2026"
111
+ * dateToMonthYearLong(new Date(2027, 0, 11)) // "enero de 2027"
112
+ */
113
+ export function dateToMonthYearLong(value) {
114
+ const yearMonth = typeof value === 'string' ? YEAR_MONTH_PATTERN.exec(value) : null;
115
+ const parts = yearMonth ? { year: Number(yearMonth[1]), month: Number(yearMonth[2]) } : toDateParts(value);
116
+ if (!parts)
117
+ return PLACEHOLDER;
118
+ return `${MONTH_NAMES[parts.month - 1]} de ${parts.year}`;
119
+ }
120
+ function toDateParts(value) {
121
+ if (value === null || value === undefined)
122
+ return null;
123
+ if (typeof value === 'string') {
124
+ const match = WALL_CLOCK_PATTERN.exec(value);
125
+ if (match) {
126
+ const [year, month, day, hours, minutes] = match.slice(1).map((part) => Number(part ?? 0));
127
+ if (!isCalendarDate(year, month, day) || hours > 23 || minutes > 59)
128
+ return null;
129
+ return { year, month, day, hours, minutes };
130
+ }
131
+ if (!ZONED_PATTERN.test(value))
132
+ return null;
133
+ // The engine rolls 2026-02-30T10:00Z over to March 2
134
+ const [year, month, day] = value.slice(0, 10).split('-').map(Number);
135
+ if (!isCalendarDate(year, month, day))
136
+ return null;
137
+ }
138
+ const time = new Date(value).getTime();
139
+ if (Number.isNaN(time))
140
+ return null;
141
+ const buenosAires = new Date(time + BUENOS_AIRES_OFFSET_MS);
142
+ return {
143
+ year: buenosAires.getUTCFullYear(),
144
+ month: buenosAires.getUTCMonth() + 1,
145
+ day: buenosAires.getUTCDate(),
146
+ hours: buenosAires.getUTCHours(),
147
+ minutes: buenosAires.getUTCMinutes(),
148
+ };
149
+ }
150
+ /** Rejects impossible dates such as 2026-02-30, which Date.UTC would roll over to March. */
151
+ function isCalendarDate(year, month, day) {
152
+ const check = new Date(Date.UTC(year, month - 1, day));
153
+ return check.getUTCFullYear() === year && check.getUTCMonth() === month - 1 && check.getUTCDate() === day;
154
+ }
155
+ function pad(value) {
156
+ return String(value).padStart(2, '0');
157
+ }
package/dist/index.d.ts CHANGED
@@ -1,4 +1,7 @@
1
1
  /**
2
2
  * Formatea un número a una cadena con dos decimales y separadores de miles.
3
3
  */
4
- export declare function numberToStringWithDecimals(numero: number): string;
4
+ export declare function numberToDecimalString(numero: number): string;
5
+ /** @deprecated Renamed to {@link numberToDecimalString}. */
6
+ export declare const numberToStringWithDecimals: typeof numberToDecimalString;
7
+ export { numberToMoney, dateToShort, dateToYMD, dateToMonthYearLong, type Currency, type NumberToMoneyOptions, type DateToShortOptions, } from './format.js';
package/dist/index.js CHANGED
@@ -1,9 +1,12 @@
1
1
  /**
2
2
  * Formatea un número a una cadena con dos decimales y separadores de miles.
3
3
  */
4
- export function numberToStringWithDecimals(numero) {
4
+ export function numberToDecimalString(numero) {
5
5
  const formatted = numero.toFixed(2);
6
6
  const [integerPart, decimalPart] = formatted.split('.');
7
7
  const withThousandsSeparator = integerPart.replace(/\B(?=(\d{3})+(?!\d))/g, '.');
8
8
  return `${withThousandsSeparator},${decimalPart}`;
9
9
  }
10
+ /** @deprecated Renamed to {@link numberToDecimalString}. */
11
+ export const numberToStringWithDecimals = numberToDecimalString;
12
+ export { numberToMoney, dateToShort, dateToYMD, dateToMonthYearLong, } from './format.js';
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "@leaderty/utils",
3
- "version": "2.0.1",
3
+ "version": "2.1.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "files": [
8
- "dist"
8
+ "dist",
9
+ "!dist/**/*.test*"
9
10
  ],
10
11
  "publishConfig": {
11
12
  "access": "public"
@@ -1 +0,0 @@
1
- export {};
@@ -1,7 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { numberToStringWithDecimals } from "./index";
3
- describe("numberToStringWithDecimals", () => {
4
- it("should format number with two decimals and thousand separators", () => {
5
- expect(numberToStringWithDecimals(1234.56)).toBe("1.234,56");
6
- });
7
- });