@ryuzaki13/react-foundation-lib 1.2.1 → 1.2.2
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/package.json +3 -1
- package/src/binary/README.md +110 -0
- package/src/file/README.md +289 -0
- package/src/file/file.mdx +6 -0
- package/src/formatters/date/README.md +459 -0
- package/src/formatters/date/date.mdx +6 -0
- package/src/formatters/number/README.md +123 -0
- package/src/formatters/number/number.mdx +6 -0
- package/src/formatters/valueState/README.md +275 -0
- package/src/formatters/valueState/valueState.mdx +6 -0
- package/src/formulas/README.md +42 -0
- package/src/formulas/formulas.mdx +6 -0
- package/src/pwa/README.md +638 -0
- package/src/pwa/pwa.mdx +6 -0
- package/src/query-client/README.md +565 -0
- package/src/query-client/queryClient.mdx +6 -0
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
# Модуль форматирования дат
|
|
2
|
+
|
|
3
|
+
Утилитарный модуль для форматирования, парсинга и простых календарных операций в "плавающей" календарной семантике.
|
|
4
|
+
Человекочитаемые форматы строятся на заранее созданных `Intl.DateTimeFormat`, чтобы переиспользовать formatter в больших таблицах и не создавать `Intl` на каждый вызов.
|
|
5
|
+
|
|
6
|
+
## Ключевая идея
|
|
7
|
+
|
|
8
|
+
Модуль намеренно не пересчитывает входные значения по timezone клиента. Для него важны видимые календарные компоненты:
|
|
9
|
+
|
|
10
|
+
- год
|
|
11
|
+
- месяц
|
|
12
|
+
- день
|
|
13
|
+
- часы
|
|
14
|
+
- минуты
|
|
15
|
+
- секунды
|
|
16
|
+
|
|
17
|
+
Например, строка `2026-03-03T18:03:50Z` будет трактоваться как "03.03.2026 18:03:50", а не как локально пересчитанное время браузера.
|
|
18
|
+
|
|
19
|
+
В настройках `Intl` поля `timeZone` и `timeZoneName` намеренно игнорируются. Даже если пользовательская предустановка передаст их явно, форматирование не должно сдвигать календарные компоненты и не должно выводить `GMT`/`UTC`-метку.
|
|
20
|
+
|
|
21
|
+
Это поведение одинаково важно для:
|
|
22
|
+
|
|
23
|
+
- ISO-строк с timezone
|
|
24
|
+
- OData ticks и OData literals
|
|
25
|
+
- timestamp-значений
|
|
26
|
+
- ручного форматирования через пресеты
|
|
27
|
+
|
|
28
|
+
## Что умеет модуль
|
|
29
|
+
|
|
30
|
+
- форматировать даты и диапазоны
|
|
31
|
+
- нормализовать `Date | null | [Date | null, Date | null]` в диапазон `Date`
|
|
32
|
+
- парсить вход из `unknown`
|
|
33
|
+
- разбирать даты по пользовательскому шаблону
|
|
34
|
+
- приводить результат парсинга к `Date | null`
|
|
35
|
+
- хранить и переопределять именованные пресеты форматирования
|
|
36
|
+
- выполнять базовые календарные операции без timezone-сдвига
|
|
37
|
+
|
|
38
|
+
## Поддерживаемые входы
|
|
39
|
+
|
|
40
|
+
- `Date`
|
|
41
|
+
- `number` с Unix timestamp в секундах или миллисекундах
|
|
42
|
+
- ISO local: `2026-03-03`, `2026-03-03T18:03:50.327`
|
|
43
|
+
- ISO zoned: `2026-03-03T18:03:50Z`, `2026-03-03T18:03:50+05:00`
|
|
44
|
+
- OData ticks: `/Date(1772524230327)/`, `/Date(1772513430327+0300)/`
|
|
45
|
+
- OData literals: `datetime'2026-03-03T18:03:50'`, `datetimeoffset'2026-03-03T18:03:50Z'`
|
|
46
|
+
- ABAP compact: `20260303`
|
|
47
|
+
- ABAP dotted: `03.03.2026`
|
|
48
|
+
- slash-date: `03/03/2026`
|
|
49
|
+
- ISO-8601 duration: `PT2H30M`, `P1DT2H3M4S`
|
|
50
|
+
|
|
51
|
+
## Встроенные пресеты
|
|
52
|
+
|
|
53
|
+
- `date`: `dd.MM.yyyy`
|
|
54
|
+
- `datetime`: `dd.MM.yyyy HH:mm`
|
|
55
|
+
- `datetime-seconds`: `dd.MM.yyyy HH:mm:ss`
|
|
56
|
+
- `time`: `HH:mm`
|
|
57
|
+
- `time-seconds`: `HH:mm:ss`
|
|
58
|
+
- `date-short`: `03.03.2026`
|
|
59
|
+
- `date-medium`: `3 мар. 2026 г.`
|
|
60
|
+
- `date-long`: `3 марта 2026 г.`
|
|
61
|
+
- `month-short`: `3 мар.`
|
|
62
|
+
- `month-long`: `3 марта`
|
|
63
|
+
- `time-short`: `18:03`
|
|
64
|
+
- `time-medium`: `18:03:50`
|
|
65
|
+
- `time-long`: `18:03:50`
|
|
66
|
+
- `datetime-short`: `03.03.2026, 18:03`
|
|
67
|
+
- `datetime-medium`: `3 мар. 2026 г., 18:03:50`
|
|
68
|
+
- `datetime-long`: `3 марта 2026 г., 18:03:50`
|
|
69
|
+
- `odata-date`: `yyyy-MM-dd`
|
|
70
|
+
- `odata-datetime`: `yyyy-MM-ddTHH:mm:ss`
|
|
71
|
+
- `abap-date`: `yyyyMMdd`
|
|
72
|
+
- `abap-datetime`: `yyyyMMddHHmmss`
|
|
73
|
+
|
|
74
|
+
Style-пресеты используют `Intl.DateTimeFormat` с `ru-RU`, но время описано явными полями. Это важно для `long`: стандартный `timeStyle: "long"` добавляет timezone, а в этом модуле timezone-семантика запрещена.
|
|
75
|
+
|
|
76
|
+
Значения по умолчанию для любого пресета:
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
{
|
|
80
|
+
locale: "ru-RU",
|
|
81
|
+
intlOptions: { day: "2-digit", month: "2-digit", year: "numeric" },
|
|
82
|
+
invalidFallback: ""
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Быстрый старт
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
import { formatDate, formatDateAsDateTime, formatDateRange, normalizeDateRange } from "@/shared/lib";
|
|
90
|
+
|
|
91
|
+
formatDate("2026-03-03T18:03:50.327Z", "datetime");
|
|
92
|
+
// "03.03.2026 18:03"
|
|
93
|
+
|
|
94
|
+
formatDateAsDateTime("/Date(1772524230327)/");
|
|
95
|
+
// "03.03.2026 18:03"
|
|
96
|
+
|
|
97
|
+
formatDateRange("2026-03-03T18:03:50Z", "2026-03-04T05:20:00+03:00", "datetime");
|
|
98
|
+
// "03.03.2026 18:03 - 04.03.2026 05:20"
|
|
99
|
+
|
|
100
|
+
normalizeDateRange(new Date(2026, 2, 3, 18, 3));
|
|
101
|
+
// [Date(2026-03-03 00:00:00), Date(2026-03-03 23:59:59)]
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Форматирование
|
|
105
|
+
|
|
106
|
+
### `formatDate`
|
|
107
|
+
|
|
108
|
+
Универсальный вход в форматирование.
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
import { formatDate } from "@/shared/lib";
|
|
112
|
+
|
|
113
|
+
formatDate("20260303", "date");
|
|
114
|
+
// "03.03.2026"
|
|
115
|
+
|
|
116
|
+
formatDate("2026-06-25", "month-short");
|
|
117
|
+
// "25 июн."
|
|
118
|
+
|
|
119
|
+
formatDate("datetimeoffset'2026-03-03T18:03:50.327Z'", "datetimeSeconds");
|
|
120
|
+
// "03.03.2026 18:03:50"
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Можно передавать не только имя пресета, но и объект пресета:
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
formatDate("2026-03-03T18:03:50", {
|
|
127
|
+
name: "machine",
|
|
128
|
+
pattern: "yyyy-MM-ddTHH:mm:ss",
|
|
129
|
+
locale: "ru-RU",
|
|
130
|
+
invalidFallback: ""
|
|
131
|
+
});
|
|
132
|
+
// "2026-03-03T18:03:50"
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Для человекочитаемых форматов лучше передавать `intlOptions`. Такой объект будет скомпилирован один раз и переиспользован через внутренний `WeakMap`, если переиспользуется сама ссылка на объект:
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
import type { DateFormatPreset } from "@/shared/lib";
|
|
139
|
+
|
|
140
|
+
const monthDatePreset = {
|
|
141
|
+
name: "month-date",
|
|
142
|
+
locale: "ru-RU",
|
|
143
|
+
intlOptions: { day: "numeric", month: "long" },
|
|
144
|
+
invalidFallback: ""
|
|
145
|
+
} satisfies DateFormatPreset;
|
|
146
|
+
|
|
147
|
+
formatDate("2026-03-03T18:03:50Z", monthDatePreset);
|
|
148
|
+
// "3 марта"
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Можно задать fallback:
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
formatDate("abc", "datetime", { fallback: "н/д" });
|
|
155
|
+
// "н/д"
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### `formatDateAsDate`, `formatDateAsMonthLong`, `formatDateAsDateTime`, `formatDateAsTime`
|
|
159
|
+
|
|
160
|
+
Упрощённые helper-обёртки поверх встроенных пресетов:
|
|
161
|
+
|
|
162
|
+
```ts
|
|
163
|
+
import { formatDateAsDate, formatDateAsDateTime, formatDateAsMonthLong, formatDateAsTime } from "@/shared/lib";
|
|
164
|
+
|
|
165
|
+
formatDateAsDate("2026-03-03T18:03:50Z");
|
|
166
|
+
// "03.03.2026"
|
|
167
|
+
|
|
168
|
+
formatDateAsMonthLong("2026-03-03T18:03:50Z");
|
|
169
|
+
// "3 марта"
|
|
170
|
+
|
|
171
|
+
formatDateAsDateTime("2026-03-03T18:03:50Z");
|
|
172
|
+
// "03.03.2026 18:03"
|
|
173
|
+
|
|
174
|
+
formatDateAsTime("2026-03-03T18:03:50Z");
|
|
175
|
+
// "18:03"
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### `formatDateRange`
|
|
179
|
+
|
|
180
|
+
Форматирует две даты в строку вида `start - end`.
|
|
181
|
+
|
|
182
|
+
```ts
|
|
183
|
+
import { formatDateRange } from "@/shared/lib";
|
|
184
|
+
|
|
185
|
+
formatDateRange("2026-03-03T18:03:50Z", "2026-03-03T22:10:00Z", "timeSeconds");
|
|
186
|
+
// "18:03:50 - 22:10:00"
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### `normalizeDateRange`, `requireDateRange`
|
|
190
|
+
|
|
191
|
+
Нормализуют UI-значение диапазона в пару дат. Одиночная дата и частично заполненный диапазон превращаются в полный календарный день.
|
|
192
|
+
Пара дат всегда упорядочивается от меньшей к большей.
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
import { normalizeDateRange, requireDateRange } from "@/shared/lib";
|
|
196
|
+
|
|
197
|
+
normalizeDateRange(new Date(2026, 2, 3, 18, 3));
|
|
198
|
+
// [Date(2026-03-03 00:00:00), Date(2026-03-03 23:59:59)]
|
|
199
|
+
|
|
200
|
+
normalizeDateRange([new Date(2026, 2, 5, 18, 3), new Date(2026, 2, 3, 5, 20)]);
|
|
201
|
+
// [Date(2026-03-03 00:00:00), Date(2026-03-05 23:59:59)]
|
|
202
|
+
|
|
203
|
+
normalizeDateRange([new Date(2026, 2, 5, 18, 3), new Date(2026, 2, 3, 5, 20)], { timeMode: "preserve" });
|
|
204
|
+
// [Date(2026-03-03 05:20:00), Date(2026-03-05 18:03:00)]
|
|
205
|
+
|
|
206
|
+
normalizeDateRange(null);
|
|
207
|
+
// null
|
|
208
|
+
|
|
209
|
+
requireDateRange(null);
|
|
210
|
+
// Error: Диапазон дат не задан
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Форматирование `duration`
|
|
214
|
+
|
|
215
|
+
`duration` поддерживается только для time-шаблонов. Если шаблон содержит токены даты, вернётся fallback.
|
|
216
|
+
|
|
217
|
+
```ts
|
|
218
|
+
formatDate("PT2H30M", "time");
|
|
219
|
+
// "02:30"
|
|
220
|
+
|
|
221
|
+
formatDate("P1DT2H3M4S", "timeSeconds");
|
|
222
|
+
// "26:03:04"
|
|
223
|
+
|
|
224
|
+
formatDate("PT12H", "date", { fallback: "н/д" });
|
|
225
|
+
// "н/д"
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
## Парсинг
|
|
229
|
+
|
|
230
|
+
### `parseDateValue`
|
|
231
|
+
|
|
232
|
+
Возвращает подробный результат с признаком типа:
|
|
233
|
+
|
|
234
|
+
```ts
|
|
235
|
+
import { parseDateValue } from "@/shared/lib";
|
|
236
|
+
|
|
237
|
+
parseDateValue("2026-03-03T18:03:50Z");
|
|
238
|
+
// {
|
|
239
|
+
// kind: "date-time",
|
|
240
|
+
// source: "iso-zoned",
|
|
241
|
+
// date: Date
|
|
242
|
+
// }
|
|
243
|
+
|
|
244
|
+
parseDateValue("PT2H30M");
|
|
245
|
+
// {
|
|
246
|
+
// kind: "duration",
|
|
247
|
+
// source: "iso-duration",
|
|
248
|
+
// durationMs: 9000000
|
|
249
|
+
// }
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Источники `source` для `kind: "date-time"`:
|
|
253
|
+
|
|
254
|
+
- `date-object`
|
|
255
|
+
- `timestamp`
|
|
256
|
+
- `odata-ticks`
|
|
257
|
+
- `odata-literal`
|
|
258
|
+
- `abap-compact`
|
|
259
|
+
- `abap-dotted`
|
|
260
|
+
- `slash-date`
|
|
261
|
+
- `iso-local`
|
|
262
|
+
- `iso-zoned`
|
|
263
|
+
|
|
264
|
+
`null` возвращается для:
|
|
265
|
+
|
|
266
|
+
- `null` и `undefined`
|
|
267
|
+
- пустых строк
|
|
268
|
+
- строк `"null"` и `"undefined"`
|
|
269
|
+
- невалидных `Date`
|
|
270
|
+
- неподдерживаемых типов
|
|
271
|
+
- битых дат и duration-строк
|
|
272
|
+
|
|
273
|
+
### `parseDateByPattern`
|
|
274
|
+
|
|
275
|
+
Парсит строку по пользовательскому шаблону с токенами:
|
|
276
|
+
|
|
277
|
+
- `dd`
|
|
278
|
+
- `MM`
|
|
279
|
+
- `yyyy`
|
|
280
|
+
- `yy`
|
|
281
|
+
|
|
282
|
+
```ts
|
|
283
|
+
import { parseDateByPattern } from "@/shared/lib";
|
|
284
|
+
|
|
285
|
+
parseDateByPattern("03/03/2026", "dd/MM/yyyy");
|
|
286
|
+
// { kind: "date-time", source: "iso-local", date: Date }
|
|
287
|
+
|
|
288
|
+
parseDateByPattern("03-03-69", "dd-MM-yy");
|
|
289
|
+
// 03.03.2069
|
|
290
|
+
|
|
291
|
+
parseDateByPattern("03+03+2026", "dd+MM+yyyy");
|
|
292
|
+
// 03.03.2026
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
Если `value` не строка, `parseDateByPattern` делегирует обработку в `parseDateValue`.
|
|
296
|
+
|
|
297
|
+
## Обёртки с результатом `Date | null`
|
|
298
|
+
|
|
299
|
+
### `parseDate`
|
|
300
|
+
|
|
301
|
+
Если нужен не подробный объект, а сразу `Date | null`, используй эту обёртку:
|
|
302
|
+
|
|
303
|
+
```ts
|
|
304
|
+
import { parseDate } from "@/shared/lib";
|
|
305
|
+
|
|
306
|
+
parseDate("2026-03-03T18:03:50Z");
|
|
307
|
+
// Date(2026-03-03 18:03:50)
|
|
308
|
+
|
|
309
|
+
parseDate("PT2H30M");
|
|
310
|
+
// Date(1970-01-01 02:30:00)
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
Для `duration` модуль строит `Date` из длительности без timezone-сдвига, сохраняя видимые UTC-компоненты. Это удобно для унификации API, но важно помнить: длительность и календарная дата не одно и то же.
|
|
314
|
+
|
|
315
|
+
Например:
|
|
316
|
+
|
|
317
|
+
- `PT2H30M` можно безопасно представить как `01.01.1970 02:30`
|
|
318
|
+
- `P1DT2H3M4S` уже становится календарной датой `02.01.1970 02:03:04`
|
|
319
|
+
- `PT30H` не сохраняет семантику "30 часов" как отдельную длительность, а превращается в календарное смещение
|
|
320
|
+
|
|
321
|
+
Если downstream-код должен различать дату и длительность, лучше использовать `parseDateValue`, а не `parseDate`.
|
|
322
|
+
|
|
323
|
+
## Работа с пресетами
|
|
324
|
+
|
|
325
|
+
### Просмотр пресетов
|
|
326
|
+
|
|
327
|
+
```ts
|
|
328
|
+
import { getDatePreset, getDatePresetNames } from "@/shared/lib";
|
|
329
|
+
|
|
330
|
+
getDatePresetNames();
|
|
331
|
+
// ["date", "datetime", "datetimeSeconds", "time", "timeSeconds"]
|
|
332
|
+
|
|
333
|
+
getDatePreset("datetime");
|
|
334
|
+
// { name: "datetime", pattern: "dd.MM.yyyy HH:mm", ... }
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
### Регистрация пользовательского пресета
|
|
338
|
+
|
|
339
|
+
```ts
|
|
340
|
+
import { formatDate, registerDatePreset } from "@/shared/lib";
|
|
341
|
+
|
|
342
|
+
registerDatePreset({
|
|
343
|
+
name: "machine",
|
|
344
|
+
pattern: "yyyy-MM-ddTHH:mm:ss"
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
formatDate("2026-03-03T18:03:50", "machine");
|
|
348
|
+
// "2026-03-03T18:03:50"
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
### Сброс пресетов и очистка кэша форматирования
|
|
352
|
+
|
|
353
|
+
```ts
|
|
354
|
+
import { clearDateFormatCache, resetDatePresets } from "@/shared/lib";
|
|
355
|
+
|
|
356
|
+
resetDatePresets();
|
|
357
|
+
clearDateFormatCache();
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
Обычно `clearDateFormatCache()` имеет смысл вызывать после массового изменения пресетов, если форматирование уже использовалось ранее.
|
|
361
|
+
|
|
362
|
+
## Календарные helper-функции
|
|
363
|
+
|
|
364
|
+
### `createCalendarDate`
|
|
365
|
+
|
|
366
|
+
Создаёт локальный `Date` без дополнительной timezone-семантики:
|
|
367
|
+
|
|
368
|
+
```ts
|
|
369
|
+
import { createCalendarDate } from "@/shared/lib";
|
|
370
|
+
|
|
371
|
+
createCalendarDate(2026, 2, 3, 18, 3, 50);
|
|
372
|
+
// 03.03.2026 18:03:50
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
Важно: месяц передаётся в формате JavaScript `Date`, то есть с нуля:
|
|
376
|
+
|
|
377
|
+
- `0` = январь
|
|
378
|
+
- `1` = февраль
|
|
379
|
+
- `2` = март
|
|
380
|
+
|
|
381
|
+
### Начало и конец периода
|
|
382
|
+
|
|
383
|
+
```ts
|
|
384
|
+
import { getEndOfDay, getStartOfDay, getStartOfMonth } from "@/shared/lib";
|
|
385
|
+
|
|
386
|
+
const value = new Date(2026, 2, 3, 18, 3, 50);
|
|
387
|
+
|
|
388
|
+
getStartOfDay(value);
|
|
389
|
+
// 03.03.2026 00:00:00
|
|
390
|
+
|
|
391
|
+
getEndOfDay(value);
|
|
392
|
+
// 03.03.2026 23:59:59
|
|
393
|
+
|
|
394
|
+
getStartOfMonth(value);
|
|
395
|
+
// 01.03.2026 00:00:00
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
### Сдвиг по календарю
|
|
399
|
+
|
|
400
|
+
```ts
|
|
401
|
+
import { addCalendarDays, addCalendarMonths, addCalendarYears } from "@/shared/lib";
|
|
402
|
+
|
|
403
|
+
const value = new Date(2026, 2, 3, 18, 3, 50);
|
|
404
|
+
|
|
405
|
+
addCalendarDays(value, 7);
|
|
406
|
+
addCalendarMonths(value, 1);
|
|
407
|
+
addCalendarYears(value, -1);
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
### Сравнение дат по календарному дню
|
|
411
|
+
|
|
412
|
+
```ts
|
|
413
|
+
import { isSameCalendarDay } from "@/shared/lib";
|
|
414
|
+
|
|
415
|
+
isSameCalendarDay(new Date(2026, 2, 3, 1, 0, 0), new Date(2026, 2, 3, 23, 59, 59));
|
|
416
|
+
// true
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
## Публичный API
|
|
420
|
+
|
|
421
|
+
Функции:
|
|
422
|
+
|
|
423
|
+
- `formatDate`
|
|
424
|
+
- `formatDateAsDate`
|
|
425
|
+
- `formatDateAsDateTime`
|
|
426
|
+
- `formatDateAsMonthLong`
|
|
427
|
+
- `formatDateAsMonthShort`
|
|
428
|
+
- `formatDateAsTime`
|
|
429
|
+
- `formatDateRange`
|
|
430
|
+
- `parseDateValue`
|
|
431
|
+
- `parseDateByPattern`
|
|
432
|
+
- `parseDate`
|
|
433
|
+
- `registerDatePreset`
|
|
434
|
+
- `getDatePreset`
|
|
435
|
+
- `getDatePresetNames`
|
|
436
|
+
- `resetDatePresets`
|
|
437
|
+
- `clearDateFormatCache`
|
|
438
|
+
- `createCalendarDate`
|
|
439
|
+
- `getStartOfDay`
|
|
440
|
+
- `getEndOfDay`
|
|
441
|
+
- `getStartOfMonth`
|
|
442
|
+
- `addCalendarDays`
|
|
443
|
+
- `addCalendarMonths`
|
|
444
|
+
- `addCalendarYears`
|
|
445
|
+
- `isSameCalendarDay`
|
|
446
|
+
|
|
447
|
+
Константы:
|
|
448
|
+
|
|
449
|
+
- `DATE_FORMAT_DEFAULTS`
|
|
450
|
+
- `DEFAULT_DATE_PRESET_NAMES`
|
|
451
|
+
|
|
452
|
+
Типы:
|
|
453
|
+
|
|
454
|
+
- `DateInputSource`
|
|
455
|
+
- `ParsedDateValue`
|
|
456
|
+
- `DateFormatPattern`
|
|
457
|
+
- `DateFormatPreset`
|
|
458
|
+
- `DateFormatPresetConfig`
|
|
459
|
+
- `FormatDateOptions`
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# Модуль форматирования чисел (presets)
|
|
2
|
+
|
|
3
|
+
Утилитарный модуль для форматирования чисел с системой предустановок.
|
|
4
|
+
Оптимизирован для массового вызова (500+ раз) — форматирование ячеек таблицы.
|
|
5
|
+
|
|
6
|
+
## Архитектура
|
|
7
|
+
|
|
8
|
+
- **Locale `ru-RU`** — неразрывный пробел для группировки, запятая для дробной части
|
|
9
|
+
- **Быстрый путь** (99% вызовов): `Intl.NumberFormat.format()` напрямую, без постобработки
|
|
10
|
+
- **Кеш** `Intl.NumberFormat` по ключу `(decimals, grouping)` — конструктор вызывается один раз
|
|
11
|
+
- **SAP-совместимость**: строки, пустые значения, ведущие нули
|
|
12
|
+
- **Неразрывный пробел** — числа не переносятся при рендере в DOM
|
|
13
|
+
- **Компактные пресеты** — короткое представление значений от `1 000` через `тыс`, `млн`, `млрд`, `трлн`
|
|
14
|
+
|
|
15
|
+
## Использование
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { formatNumber, registerPreset } from "@/shared/lib";
|
|
19
|
+
|
|
20
|
+
formatNumber(1234567, "Integer"); // "1 234 567" (неразрывные пробелы)
|
|
21
|
+
formatNumber(1234567.891, "Decimal"); // "1 234 567,9"
|
|
22
|
+
formatNumber(99.1, "Percent"); // "99,10"
|
|
23
|
+
formatNumber(1534000, "compact-currency"); // "1,5 млн"
|
|
24
|
+
formatNumber("00123.4", "Decimal"); // "123,4"
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## API
|
|
28
|
+
|
|
29
|
+
### `formatNumber(value, presetOrName)`
|
|
30
|
+
|
|
31
|
+
| Параметр | Тип | Описание |
|
|
32
|
+
| -------------- | ------------------------------ | -------------------------------------------------- |
|
|
33
|
+
| `value` | `number \| string` | Число или строковое представление (SAP-совместимо) |
|
|
34
|
+
| `presetOrName` | `string \| NumberFormatPreset` | Имя предустановки или объект с параметрами |
|
|
35
|
+
|
|
36
|
+
Можно передать объект напрямую, без регистрации:
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
formatNumber(1234567.891, {
|
|
40
|
+
name: "custom",
|
|
41
|
+
decimals: 3,
|
|
42
|
+
decimalSeparator: ".",
|
|
43
|
+
grouping: true,
|
|
44
|
+
groupingSeparator: ",",
|
|
45
|
+
groupingSize: 3
|
|
46
|
+
});
|
|
47
|
+
// "1,234,567.891"
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### `registerPreset(config)` / `getPreset(name)` / `getPresetNames()`
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
registerPreset({ name: "Weight", decimals: 3, decimalSeparator: "." });
|
|
54
|
+
formatNumber(9876.5, "Weight"); // "9 876.500"
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### `resetPresets()` / `clearFormatCache()`
|
|
58
|
+
|
|
59
|
+
Сброс реестра и кеша соответственно.
|
|
60
|
+
|
|
61
|
+
## Типовые предустановки
|
|
62
|
+
|
|
63
|
+
| Имя | decimals | decimalSeparator | Пример |
|
|
64
|
+
| ------------------------ | -------- | ---------------- | ------------- |
|
|
65
|
+
| `Integer` | 0 | — | `1 234 567` |
|
|
66
|
+
| `Decimal` | 1 | `,` | `1 234 567,9` |
|
|
67
|
+
| `Currency` | 0 | — | `5 000 000` |
|
|
68
|
+
| `Price` | 0 | — | `42 000` |
|
|
69
|
+
| `Percent` | 2 | `,` | `99,10` |
|
|
70
|
+
| `Tonnage` | 1 | `,` | `15 000,8` |
|
|
71
|
+
| `compact-currency` | 0 | — | `1,5 млн` |
|
|
72
|
+
| `compact-currency-round` | 0 | — | `35 тыс` |
|
|
73
|
+
| `compact-percent` | 2 | `,` | `12,34 тыс` |
|
|
74
|
+
| `compact-percent-round` | 2 | `,` | `12,35 тыс` |
|
|
75
|
+
| `compact-tonnage` | 1 | `,` | `9,5 млн` |
|
|
76
|
+
| `compact-tonnage-round` | 1 | `,` | `9,6 млн` |
|
|
77
|
+
|
|
78
|
+
Все типовые предустановки используют `grouping: true`, `groupingSeparator` — неразрывный пробел из `ru-RU`, `groupingSize: 3`.
|
|
79
|
+
|
|
80
|
+
Предустановки без суффикса `-round` усекают компактную часть к нулю. Это
|
|
81
|
+
поведение подходит для консервативных подписей шкалы. Парные предустановки
|
|
82
|
+
`compact-*-round` математически округляют компактную часть и предназначены
|
|
83
|
+
для случаев, где подпись должна отражать ближайшее значение.
|
|
84
|
+
|
|
85
|
+
## Значения по умолчанию (`FORMAT_DEFAULTS`)
|
|
86
|
+
|
|
87
|
+
| Свойство | Значение | Описание |
|
|
88
|
+
| ------------------- | --------------- | ------------------------- |
|
|
89
|
+
| `decimals` | `1` | Количество десятичных |
|
|
90
|
+
| `decimalSeparator` | `","` | Разделитель дробной части |
|
|
91
|
+
| `grouping` | `true` | Группировка разрядов |
|
|
92
|
+
| `groupingSeparator` | NBSP из `ru-RU` | Неразрывный пробел |
|
|
93
|
+
| `groupingSize` | `3` | Размер группы |
|
|
94
|
+
|
|
95
|
+
## Типы
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
interface NumberFormatPreset {
|
|
99
|
+
name: string;
|
|
100
|
+
decimals: number;
|
|
101
|
+
decimalSeparator: string;
|
|
102
|
+
grouping: boolean;
|
|
103
|
+
groupingSeparator: string;
|
|
104
|
+
groupingSize: number;
|
|
105
|
+
compact?: NumberFormatCompactOptions;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
type NumberFormatCompactOptions = {
|
|
109
|
+
minCompactValue?: number;
|
|
110
|
+
maxDecimals?: number;
|
|
111
|
+
roundingMode?: "round" | "floor" | "ceil" | "trunc";
|
|
112
|
+
suffixSeparator?: string;
|
|
113
|
+
units?: readonly { value: number; suffix: string }[];
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
type NumberFormatPresetConfig = Partial<Omit<NumberFormatPreset, "name">> & Pick<NumberFormatPreset, "name">;
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Три ветки форматирования
|
|
120
|
+
|
|
121
|
+
1. **Стандартный ru-RU** (NBSP + запятая, groupingSize=3): `format()` напрямую
|
|
122
|
+
2. **Кастомные разделители** (groupingSize=3): `formatToParts()` + подстановка
|
|
123
|
+
3. **Нестандартный groupingSize**: Intl-округление + ручная группировка
|