@ryuzaki13/react-foundation-lib 1.2.4 → 1.2.6

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.
Files changed (35) hide show
  1. package/dist/chunks/{dateUtils-BWIaWh13.js → dateUtils-B5V5aXB-.js} +225 -201
  2. package/dist/chunks/dateUtils-B5V5aXB-.js.map +1 -0
  3. package/dist/chunks/{formatters-CEsXVZ7n.js → formatters--mhLir82.js} +4 -4
  4. package/dist/chunks/{formatters-CEsXVZ7n.js.map → formatters--mhLir82.js.map} +1 -1
  5. package/dist/chunks/{odata-service-D8oFjUah.js → odata-service-DnPlNGpU.js} +2 -2
  6. package/dist/chunks/{odata-service-D8oFjUah.js.map → odata-service-DnPlNGpU.js.map} +1 -1
  7. package/dist/chunks/{validate-DrKdjesg.js → validate-nWY_Mla5.js} +2 -2
  8. package/dist/chunks/{validate-DrKdjesg.js.map → validate-nWY_Mla5.js.map} +1 -1
  9. package/dist/error-report/index.js +2 -2
  10. package/dist/excel/index.js +3 -3
  11. package/dist/formatters/date/index.d.ts +1 -1
  12. package/dist/formatters/date/index.d.ts.map +1 -1
  13. package/dist/formatters/date/parseDate.d.ts +13 -1
  14. package/dist/formatters/date/parseDate.d.ts.map +1 -1
  15. package/dist/formatters/index.js +5 -5
  16. package/dist/odata/index.js +1 -1
  17. package/dist/odata-service/index.js +1 -1
  18. package/package.json +1 -1
  19. package/src/docs/browserAndReact.mdx +150 -0
  20. package/src/docs/filesAndExcel.mdx +163 -0
  21. package/src/docs/formatters-runtime.mdx +455 -0
  22. package/src/docs/formatters.mdx +363 -0
  23. package/src/docs/odataAndTables.mdx +178 -0
  24. package/src/docs/pureUtilities.mdx +96 -0
  25. package/src/docs/runtimeAndState.mdx +155 -0
  26. package/src/formatters/date/README.md +36 -2
  27. package/src/foundationLib.mdx +105 -0
  28. package/dist/chunks/dateUtils-BWIaWh13.js.map +0 -1
  29. package/src/file/file.mdx +0 -6
  30. package/src/formatters/date/date.mdx +0 -6
  31. package/src/formatters/number/number.mdx +0 -6
  32. package/src/formatters/valueState/valueState.mdx +0 -6
  33. package/src/formulas/formulas.mdx +0 -6
  34. package/src/pwa/pwa.mdx +0 -6
  35. package/src/query-client/queryClient.mdx +0 -6
@@ -0,0 +1,363 @@
1
+ import { Meta } from "@storybook/addon-docs/blocks";
2
+
3
+ <Meta title="Foundation Lib/Formatters" />
4
+
5
+ # Форматтеры
6
+
7
+ `@ryuzaki13/react-foundation-lib/formatters` — единая публичная точка входа для
8
+ преобразования значений в host-проекте. Она покрывает даты, числа, строки,
9
+ логические значения, ФИО, телефон и числовые идентификаторы. Импортировать
10
+ внутренние файлы пакета и корневой пакет не нужно: все показанные ниже имена
11
+ доступны из опубликованного subpath.
12
+
13
+ ```ts
14
+ import { formatDate, formatNumber, normalizeText, parseBoolean } from "@ryuzaki13/react-foundation-lib/formatters";
15
+
16
+ import type { DateFormatPreset, NumberFormatPresetConfig } from "@ryuzaki13/react-foundation-lib/formatters";
17
+ ```
18
+
19
+ ## Выбор API
20
+
21
+ | Задача | Используйте |
22
+ | ------------------------------------------------------------ | -------------------------------------------------------------- |
23
+ | Отобразить или разобрать дату без неявного timezone-сдвига | `formatDate`, `parseDate`, `parseDateTZ` |
24
+ | Получить календарные границы или безопасный диапазон | `getCalendarPeriod`, `normalizeDateRange`, `requireDateRange` |
25
+ | Отобразить число или разобрать SAP-подобную строку | `formatNumber`, `toFiniteNumber`, `parseNumber` |
26
+ | Привести текст к безопасной строке или нормализовать пробелы | `toSafeString`, `normalizeText`, `normalizeTextSpaces` |
27
+ | Преобразовать ABAP boolean | `parseAbapBoolean`, `toAbapBoolean` |
28
+ | Показать ФИО, российский номер или код с ведущими нулями | `formatFullName`, `formatPhone`, `normalizeLeadingZerosStrict` |
29
+
30
+ ## Даты
31
+
32
+ ### Основной форматтер
33
+
34
+ ```ts
35
+ import { formatDate, formatDateAsDateTime, formatDateAsODataDate } from "@ryuzaki13/react-foundation-lib/formatters";
36
+
37
+ formatDate("2026-03-05T14:30:00", "date");
38
+ // "05.03.2026"
39
+
40
+ formatDateAsDateTime("/Date(1772713800000+0500)/");
41
+ // строка даты и времени в локальной календарной семантике
42
+
43
+ formatDateAsODataDate(new Date(2026, 2, 5));
44
+ // "2026-03-05"
45
+ ```
46
+
47
+ Сигнатура универсального вызова:
48
+
49
+ ```ts
50
+ formatDate(
51
+ value: unknown,
52
+ presetOrName?: string | DateFormatPreset,
53
+ options?: FormatDateOptions
54
+ ): string;
55
+ ```
56
+
57
+ `value` может быть `Date`, Unix timestamp, OData ticks вида
58
+ `/Date(…)/`, OData literal, ABAP-дата/временная метка, ISO-строка,
59
+ ISO-длительность и несколько распространённых человекочитаемых форматов.
60
+ Неподходящее значение не выбрасывает исключение: результатом будет
61
+ `options.fallback`, либо `invalidFallback` выбранного пресета, либо пустая
62
+ строка по умолчанию.
63
+
64
+ Исключение — неизвестное строковое имя пресета: `formatDate` выбросит
65
+ `Error`. Если формат получен из внешней конфигурации, сначала разрешите его
66
+ через `resolveDateFormatPreset` или проверьте через `getDatePreset`.
67
+
68
+ `FormatDateOptions`:
69
+
70
+ | Поле | Назначение |
71
+ | ---------------------------------------- | -------------------------------------------------------------------------- |
72
+ | `fallback?: string` | Результат для нераспознанного входа; имеет приоритет над fallback пресета. |
73
+ | `precision?: "day" \| "month" \| "year"` | Скрывает календарные части ниже выбранной точности. |
74
+
75
+ ### Готовые форматтеры
76
+
77
+ Все функции принимают `value: unknown` и необязательный `FormatDateOptions`,
78
+ возвращают строку и используют соответствующий встроенный пресет.
79
+
80
+ | API | Результат / назначение |
81
+ | --------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
82
+ | `formatDateAsDate` | `dd.MM.yyyy`. |
83
+ | `formatDateAsDateTime` | Дата и время до минут. |
84
+ | `formatDateAsTime`, `formatDateAsTimeSeconds` | Время до минут или секунд. |
85
+ | `formatDateAsDateShort`, `formatDateAsDateMedium`, `formatDateAsDateLong` | Числовая, сокращённая и полная русская дата. |
86
+ | `formatDateAsMonthShort`, `formatDateAsMonthMedium`, `formatDateAsMonthLong` | День и месяц без года: числовой, сокращённый или полный вид. |
87
+ | `formatDateAsMonthYearMedium`, `formatDateAsMonthYearLong` | Месяц и год в сокращённом или полном виде. |
88
+ | `formatDateAsTimeShort`, `formatDateAsTimeMedium`, `formatDateAsTimeLong` | Короткое или секундное время без вывода timezone. |
89
+ | `formatDateAsDatetimeShort`, `formatDateAsDatetimeMedium`, `formatDateAsDatetimeLong` | Дата и время с разной детализацией. |
90
+ | `formatDateAsAbapYear`, `formatDateAsAbapMonth`, `formatDateAsAbapDate`, `formatDateAsAbapDatetime` | Машинные строки `yyyy`, `yyyyMM`, `yyyyMMdd`, `yyyyMMddHHmmss`. |
91
+ | `formatDateAsODataDate`, `formatDateAsODataDatetime` | Машинные строки `yyyy-MM-dd`, `yyyy-MM-ddTHH:mm:ss`. |
92
+ | `formatDateAsODataTime` | Время как OData duration `PT00H00M00S`; для ISO-длительности форматирует её как время. |
93
+ | `formatDateRange(startValue, endValue, presetOrName?, options?)` | Две валидные даты через `-`; если любая невалидна, возвращает fallback. |
94
+
95
+ ### Разбор даты и timezone-семантика
96
+
97
+ ```ts
98
+ import { parseDate, parseDateTZ, parseDateByPattern, parseDateValue } from "@ryuzaki13/react-foundation-lib/formatters";
99
+
100
+ parseDate("20260305");
101
+ // Date | null
102
+
103
+ parseDateTZ("2026-03-05T10:00:00+05:00");
104
+ // Date | null, сохраняет абсолютный instant
105
+
106
+ parseDateByPattern("05/03/2026", "dd/MM/yyyy");
107
+ // { kind: "date-time", source: "iso-local", date: Date } | null
108
+
109
+ parseDateValue("PT01H30M");
110
+ // { kind: "duration", source: "iso-duration", durationMs: 5_400_000 } | null
111
+ ```
112
+
113
+ | API | Вход и результат |
114
+ | ----------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
115
+ | `parseDateValue(value)` | `ParsedDateValue \| null`. Обычный режим: сохраняет видимые календарные компоненты без применения timezone. |
116
+ | `parseDateValueTZ(value)` | `ParsedDateValue \| null`. Для Unix timestamp и ISO/OData со смещением сохраняет абсолютный момент времени. |
117
+ | `parseDate(value)` | `Date \| null`; в том числе приводит duration к `Date` в «плавающей» календарной семантике. |
118
+ | `parseDateTZ(value)` | `Date \| null` с timezone-aware трактовкой абсолютного времени. |
119
+ | `parseDateByPattern(value, pattern?, { precision? })` | Строго разбирает токены `dd`, `MM`, `yyyy`, `yy`, `HH`, `mm`, `ss`; невалидные компоненты дают `null`. Двузначный год `00–69` — это `2000–2069`, `70–99` — `1970–1999`. |
120
+ | `parseDateByFormat(value, dateFormat?, options?)` | Разбирает зарегистрированное имя пресета, style-алиас или ручной шаблон. `options`: `defaultFormat`, `locale`, `precision`, `referenceDate`. |
121
+
122
+ Публичные типы для результатов и контракта: `DateInputSource`,
123
+ `ParsedDateValue`, `DateFormatPattern`, `DateFormatStyle`,
124
+ `DateFormatPrecision`, `DateFormatPreset`, `DateFormatPresetConfig`,
125
+ `FormatDateOptions`, `ParseDateByPatternOptions`, `ParseDateByFormatOptions`.
126
+
127
+ Не заменяйте `parseDate` на `new Date("YYYY-MM-DD")`, если значение является
128
+ календарной датой: native parsing может изменить отображаемый день из-за
129
+ timezone.
130
+
131
+ ### Пресеты дат
132
+
133
+ ```ts
134
+ import {
135
+ DEFAULT_DATE_PRESET_NAMES,
136
+ getDatePreset,
137
+ registerDatePreset,
138
+ resolveDateFormatPreset
139
+ } from "@ryuzaki13/react-foundation-lib/formatters";
140
+
141
+ registerDatePreset({
142
+ name: "report-date",
143
+ pattern: "dd.MM.yyyy",
144
+ invalidFallback: "—"
145
+ });
146
+
147
+ formatDate("2026-03-05", "report-date"); // "05.03.2026"
148
+
149
+ const safePreset = resolveDateFormatPreset("dd/MM/yyyy");
150
+ formatDate("05/03/2026", safePreset); // "05/03/2026"
151
+ ```
152
+
153
+ `DEFAULT_DATE_PRESET_NAMES` содержит: `date`, `date-short`, `date-medium`,
154
+ `date-long`, `month-short`, `month-medium`, `month-long`,
155
+ `month-year-medium`, `month-year-long`, `datetime`, `datetime-seconds`,
156
+ `datetime-short`, `datetime-medium`, `datetime-long`, `time`,
157
+ `time-seconds`, `time-short`, `time-medium`, `time-long`, `odata-date`,
158
+ `odata-datetime`, `abap-datetime`, `abap-date`, `abap-month`, `abap-year`.
159
+
160
+ | API | Поведение |
161
+ | ---------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
162
+ | `DATE_FORMAT_DEFAULTS` | Значения по умолчанию: `ru-RU`, числовая дата и пустой `invalidFallback`. |
163
+ | `DATE_FORMAT_STYLE_PRESET_NAMES` | Соответствие `short`, `medium`, `long` встроенным date-пресетам. |
164
+ | `getDatePreset(name)`, `getDatePresetNames()` | Возвращают пресет или `undefined`, список имён. |
165
+ | `registerDatePreset(config)` | Компилирует и регистрирует/перезаписывает пресет, возвращает готовый `DateFormatPreset`. Реестр module-scoped. |
166
+ | `resetDatePresets()` | Сбрасывает реестр к встроенным пресетам; используйте для изолированных тестов, не в обычном runtime. |
167
+ | `isDateFormatStyle(value)`, `isDateFormatPrecision(value)` | Type guard для style и precision. |
168
+ | `resolveDateFormatName(value?, defaultFormat?)` | Обрезает пробелы и переводит `short` / `medium` / `long` в имя date-пресета. |
169
+ | `resolveDateFormatPreset(value?, options?)` | Возвращает имя зарегистрированного пресета или кешируемый объектный пресет для ручного шаблона. |
170
+
171
+ Для ручных шаблонов доступны константы `DATE_PATTERN_DATE_TOKENS`,
172
+ `DATE_PATTERN_TIME_TOKENS`, `DATE_PATTERN_TOKENS` и `DATE_PATTERN_TOKEN_RE`.
173
+ Поддерживаются только перечисленные токены; остальные фрагменты шаблона —
174
+ литеральный текст.
175
+
176
+ ### Календарные значения и диапазоны
177
+
178
+ Эти функции работают с локальными компонентами `Date` и не добавляют UTC- или
179
+ timezone-семантику.
180
+
181
+ | API | Результат / fallback |
182
+ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
183
+ | `createCalendarDate(year, monthIndex, day, hour?, minute?, second?, millisecond?)` | Новый локальный `Date`; `monthIndex` соответствует `Date#getMonth()`. |
184
+ | `getStartOfDay`, `getEndOfDay`, `getStartOfMonth`, `getStartOfYear`, `getStartOfWeek(date, weekStartsOn?)` | Новая дата с календарной границей; по умолчанию неделя начинается в понедельник. |
185
+ | `addCalendarDays`, `addCalendarMonths`, `addCalendarYears` | Новый `Date`, сдвинутый в календарной семантике. |
186
+ | `isSameCalendarDay(left, right)` | `false`, если любой аргумент не является `Date`; иначе сравнивает год, месяц, день. |
187
+ | `getCalendarPeriod(value, { selectionMode?, weekEndDay? })` | `{ start, end } \| null`; режимы `day`, `week`, `month`, `year`, конец недели — `friday`, `saturday` или `sunday`. |
188
+ | `isDateInsideCalendarPeriod`, `isCalendarPeriodWithinDateBounds` | Проверяют включительные границы; при отсутствующих/невалидных данных возвращают `false`. |
189
+ | `resolveDateRangePair(value)` | Упорядоченная пара дат или `null`; одиночная дата становится диапазоном из неё самой. |
190
+ | `normalizeDateRange(value, { timeMode? })` | `DateRange \| null`; по умолчанию расширяет до границ суток, `preserve` сохраняет время и создаёт копии. |
191
+ | `requireDateRange(value, options?)` | Как `normalizeDateRange`, но при отсутствии диапазона выбрасывает `Error`. |
192
+ | `countCalendarDaysInDateRange(value)` | Количество дней включительно или `null`. |
193
+ | `isValidDate`, `isDateRangeTuple`, `cloneDate`, `orderDates` | Низкоуровневые guards и операции диапазона. |
194
+ | `normalizeDateRangeReferenceDate`, `resolveTodayRange`, `resolveYesterdayRange`, `resolveMonthAgoRange`, `resolveMonthStartToTodayRange`, `resolveMonthStartToYesterdayRange` | Относительные диапазоны от `{ referenceDate }`; на первом дне месяца «по вчера» не создаёт инверсию. |
195
+
196
+ Публичные типы диапазонов: `CalendarPeriod`, `CalendarPeriodOptions`,
197
+ `CalendarPeriodDateBounds`, `CalendarPeriodSelectionMode`,
198
+ `CalendarWeekEndDay`, `DateRange`, `NullableDateRange`, `DateRangeInput`,
199
+ `DateRangeTimeMode`, `NormalizeDateRangeOptions`, `DateRangeReferenceContext`.
200
+
201
+ ### Учебный календарь
202
+
203
+ Для образовательной модели доступны `EducationDateKey` (`YYYY-MM-DD`),
204
+ `EducationYearRange` и `EducationWeekRange`.
205
+
206
+ | API | Поведение |
207
+ | ---------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
208
+ | `parseEducationDateKey`, `createEducationDateTime` | Возвращают `Date \| null` при неверном ключе или времени. |
209
+ | `formatEducationDateKey`, `formatEducationTimeKey` | Создают стабильные локальные ключи `YYYY-MM-DD` и `HH:mm`. |
210
+ | `getEducationYear`, `getEducationYearRange`, `isEducationDateInCurrentYear` | Учебный год: 1 сентября — 31 августа. |
211
+ | `getEducationWeekStartDate`, `getEducationWeekRange`, `getEducationWeekDays`, `getCurrentEducationWeekKey` | Учебная неделя: понедельник — суббота; воскресенье относится к следующей. |
212
+ | `normalizeEducationWeekKey`, `resolveEducationWeekKey`, `isCurrentEducationWeek` | Невалидный или вне текущего учебного года ключ даёт `undefined`; `resolve…` вместо этого возвращает текущую неделю. |
213
+
214
+ ## Числа
215
+
216
+ ### Форматирование и пресеты
217
+
218
+ ```ts
219
+ import {
220
+ formatNumber,
221
+ formatNumberAsDecimal2,
222
+ formatNumberAsIntegerOrEmpty,
223
+ registerNumberPreset
224
+ } from "@ryuzaki13/react-foundation-lib/formatters";
225
+
226
+ formatNumberAsDecimal2(12345.6); // "12 345,60" в ru-RU
227
+ formatNumberAsIntegerOrEmpty("0"); // ""
228
+
229
+ registerNumberPreset({
230
+ name: "quantity-4",
231
+ decimals: 4,
232
+ grouping: false
233
+ });
234
+
235
+ formatNumber(12.5, "quantity-4"); // "12,5000"
236
+ ```
237
+
238
+ ```ts
239
+ formatNumber(value: unknown, presetOrName: string | NumberFormatPreset): string;
240
+ ```
241
+
242
+ Форматирование использует `Intl.NumberFormat("ru-RU")`. Пустая строка,
243
+ `NaN`, `Infinity` и значения, которые `Number(value)` не может привести к
244
+ конечному числу, возвращают строку `"0"`. Неизвестное строковое имя пресета
245
+ выбрасывает `Error`.
246
+
247
+ `formatNumber` не является SAP-парсером: переданная строка `"1 234,5"` не
248
+ обязана быть принята `Number`. Для такого входа сначала используйте
249
+ `toFiniteNumber` или `parseNumber`.
250
+
251
+ | API | Поведение |
252
+ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
253
+ | `formatNumberAsInteger`, `formatNumberAsCurrency`, `formatNumberAsPrice` | Встроенные пресеты с нулевой дробной частью. Валютный знак не добавляется. |
254
+ | `formatNumberAsDecimal`, `formatNumberAsDecimal2`, `formatNumberAsDecimal3` | Один, два или три знака после запятой. |
255
+ | `formatNumberAsPercent`, `formatNumberAsTonnage` | Встроенные пресеты с двумя и одним знаком соответственно; знак `%` не добавляется. |
256
+ | `formatNumberAsIntegerOrEmpty`, `formatNumberAsCurrencyOrEmpty`, `formatNumberAsDecimalOrEmpty`, `formatNumberAsDecimal2OrEmpty`, `formatNumberAsDecimal3OrEmpty`, `formatNumberAsPercentOrEmpty`, `formatNumberAsPriceOrEmpty`, `formatNumberAsTonnageOrEmpty` | Сначала проверяют семантический ноль; для него возвращают `""`, иначе используют одноимённый форматтер. |
257
+ | `truncateNumber(number, decimals)` | Усечение дробной части математически, без форматирования и без проверки аргументов. |
258
+ | `clearFormatCache()` | Очищает module-scoped кеш `Intl.NumberFormat`; обычно нужен только для тестов или смены изолированного окружения. |
259
+
260
+ `DEFAULT_NUMBER_PRESET_NAMES` содержит: `integer`, `decimal`, `decimal-2`,
261
+ `decimal-3`, `currency`, `price`, `percent`, `tonnage`,
262
+ `compact-currency`, `compact-currency-round`, `compact-percent`,
263
+ `compact-percent-round`, `compact-tonnage`, `compact-tonnage-round`.
264
+ Компактность задаётся через поле `compact` самого пресета: при значении от
265
+ 1000 по умолчанию используются суффиксы `тыс`, `млн`, `млрд`, `трлн`.
266
+
267
+ | API / тип | Назначение |
268
+ | ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------ |
269
+ | `NUMBER_FORMAT_DEFAULTS` | Базовые значения: один знак, `,`, группировка ru-RU, размер группы 3. |
270
+ | `NumberFormatPreset`, `NumberFormatPresetConfig` | Контракт пресета и конфигурации регистрации. |
271
+ | `NumberFormatCompactOptions`, `NumberFormatCompactUnit`, `NumberFormatCompactRoundingMode` | Настройки compact-представления: минимум, дробная часть, режим `round` / `floor` / `ceil` / `trunc`, суффиксы и единицы. |
272
+ | `getNumberPreset(name)`, `getNumberPresetNames()` | Чтение пресета или `undefined`, список имён. |
273
+ | `registerNumberPreset(config)` | Регистрирует или перезаписывает пресет, дополняя отсутствующие поля `NUMBER_FORMAT_DEFAULTS`. |
274
+ | `resetNumberPresets()` | Возвращает только встроенные пресеты; не вызывайте в работающем приложении. |
275
+
276
+ ### Разбор и проверки
277
+
278
+ ```ts
279
+ import { isPositiveValue, parseNumber, toFiniteNumber, toPositiveInteger } from "@ryuzaki13/react-foundation-lib/formatters";
280
+
281
+ toFiniteNumber("1 234,50"); // 1234.5
282
+ parseNumber("не число"); // 0
283
+ toPositiveInteger("12"); // 12
284
+ isPositiveValue("0"); // false
285
+ ```
286
+
287
+ | API | Fallback |
288
+ | -------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
289
+ | `toFiniteNumber(value)` | `number \| undefined`. Поддерживает пробелы, NBSP, узкий NBSP и `'` как группировку; запятую как десятичный разделитель. |
290
+ | `parseNumber(value)` | Как `toFiniteNumber`, но возвращает `0` вместо `undefined`. |
291
+ | `toPositiveInteger(value)` | Положительное целое или `undefined`. |
292
+ | `isPositiveValue(value)` | `true` только для конечного значения строго больше нуля. |
293
+ | `isZeroValue(value)` | `true` для значений, семантически равных нулю; используется вариантами `…OrEmpty`. |
294
+
295
+ ## Строки
296
+
297
+ | API | Вход, результат и fallback |
298
+ | ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
299
+ | `normalizeText(value)` | Обрезанная непустая строка или `undefined` для нестрокового/пустого значения. |
300
+ | `normalizeTextWithFallback(value, fallback = "")` | Нормализованный текст или указанный fallback. |
301
+ | `normalizeRequiredText(value)` | Нормализованный текст или `""`. |
302
+ | `normalizeTextToLower(value)` | Нормализованный текст в нижнем регистре или `""`. |
303
+ | `normalizeTextSpaces(value)` | Убирает пробелы по краям и сворачивает пробельные символы, NBSP и узкие NBSP в один обычный пробел. |
304
+ | `toSafeString(value)` | `""` для `null`/`undefined`, иначе `String(value)`. |
305
+ | `stripInnerSpaces(value)` | Удаляет только обычные, неразрывные и узкие неразрывные пробелы внутри строки. |
306
+ | `stripLeadingZeros(value)` | Для целых цифровых строк удаляет ведущие нули, оставляя один ноль; поддерживает минус. Нечисловая строка возвращается с обрезанными краями. |
307
+ | `startsWithIgnoringZeros(value, searchText)` | Case-insensitive проверка префикса после удаления ведущих нулей у обеих строк. |
308
+ | `truncateText(value, maxLength)` | Обрезает нормализованный текст и добавляет `...`; пустой/невалидный текст даёт `undefined`. |
309
+
310
+ ## Логические значения ABAP
311
+
312
+ ```ts
313
+ import { parseAbapBoolean, parseBoolean, toAbapBoolean } from "@ryuzaki13/react-foundation-lib/formatters";
314
+
315
+ parseAbapBoolean("X"); // true
316
+ parseBoolean("true"); // true
317
+ toAbapBoolean(false); // " "
318
+ ```
319
+
320
+ | API | Поведение |
321
+ | ------------------------- | --------------------------------------------------------------------------------------------------------------- |
322
+ | `parseAbapBoolean(value)` | Возвращает `true` только для строкового точного `"X"`; всё остальное — `false`. |
323
+ | `parseBoolean(value)` | Для строк регистронезависимо принимает `"true"`, `"x"`, `"1"`; для остальных значений использует JS truthiness. |
324
+ | `toAbapBoolean(value)` | Возвращает ABAP `"X"` или `" "`. Строка `"0"` — ложь; любая другая строка, включая `""`, — истина. |
325
+
326
+ ## ФИО, телефон и ведущие нули
327
+
328
+ ### ФИО и телефон
329
+
330
+ ```ts
331
+ import { clearPhone, formatFullName, formatPhone, formatShortName } from "@ryuzaki13/react-foundation-lib/formatters";
332
+
333
+ formatFullName(" иванов иван иванович "); // "Иванов Иван Иванович"
334
+ formatShortName("иванов иван иванович"); // "Иванов И.И."
335
+ formatPhone("8 (912) 345-67-89"); // "+7 (912) 345-67-89"
336
+ ```
337
+
338
+ | API | Ограничения и fallback |
339
+ | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
340
+ | `formatFullName(fullName)` | Нормализует пробелы, нижний регистр и регистр первых трёх частей. Это presentation helper, а не валидатор ФИО; дополнительные части игнорируются. |
341
+ | `formatShortName(fullName)` | Показывает фамилию и одну/две инициалы. Пустая строка даёт `""`. |
342
+ | `clearPhone(number)` | Оставляет цифры, ограничивает ввод первыми 11 цифрами, приводит начальный `7`/`8` к `+7`. Предназначен для российских номеров. |
343
+ | `formatPhone(number)` | Форматирует только полный нормализованный номер `+7` и 10 цифр. Если после очистки номер неполный, возвращает исходную строку без изменений. |
344
+
345
+ ### Числовые идентификаторы
346
+
347
+ ```ts
348
+ import { normalizeLeadingZeros, normalizeLeadingZerosStrict } from "@ryuzaki13/react-foundation-lib/formatters";
349
+
350
+ normalizeLeadingZeros("0012", 5); // "00012"
351
+ normalizeLeadingZerosStrict("0012", -1); // "12"
352
+ normalizeLeadingZerosStrict("12,5", 4); // "0012,5"
353
+ ```
354
+
355
+ | API | Поведение |
356
+ | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
357
+ | `normalizeLeadingZeros(value, fixed?)` | Приводит конечное числовое значение через `Number` к строке; при `fixed > 0` дополняет целую часть нулями. Нечисловое/бесконечное значение возвращает как есть. Поэтому этот вариант может изменить строковое представление и тип результата. |
358
+ | `normalizeLeadingZerosStrict(value, fixed?)` | Сохраняет тип `T`: при `fixed` отсутствующем, нулевом или не конечном — no-op; при `fixed < 0` удаляет нули в целой части строки, при `fixed > 0` дополняет. Не распознанная числовая строка возвращается без изменений. Для `number` ведущие нули представить невозможно, поэтому результат остаётся числом. |
359
+
360
+ Для пользовательского draft не вызывайте нормализацию во время ввода. Храните
361
+ буквальное значение, а `normalizeLeadingZerosStrict` применяйте только на
362
+ явной границе сохранения или восстановления, если это действительно часть
363
+ контракта данных.
@@ -0,0 +1,178 @@
1
+ import { Meta } from "@storybook/addon-docs/blocks";
2
+
3
+ <Meta title="Foundation Lib/OData, таблицы и формулы" />
4
+
5
+ # OData metadata, таблицы и формулы
6
+
7
+ Здесь находятся чистые OData V2 helpers и runtime contracts для TanStack Table. Они не выполняют запросы. Для SAP Gateway transport, SSO, X-CSRF, metadata fetch и TanStack Query operation используйте `@ryuzaki13/react-foundation-api/odata`.
8
+
9
+ ## `odata-service`: metadata и OData V2 values
10
+
11
+ ```ts
12
+ import {
13
+ buildEntityPath,
14
+ buildODataFilter,
15
+ buildODataOrderBy,
16
+ odataFormatValue,
17
+ parseServiceMetadata,
18
+ wrapODataParams
19
+ } from "@ryuzaki13/react-foundation-lib/odata-service";
20
+ ```
21
+
22
+ ### Основной поток
23
+
24
+ ```text
25
+ metadata XML → parseServiceMetadata → typed metadata/config
26
+ → wrapODataParams + builder/filter/sort → foundation-api transport
27
+ → metadata-aware parse/format → entity/feature mapping
28
+ ```
29
+
30
+ Не передавайте в helpers невалидный external JSON как будто он уже соответствует domain model. Metadata parser описывает OData contract, а domain validation/mapping остаётся у host entity/feature.
31
+
32
+ ### Metadata, paths и параметры
33
+
34
+ | API | Результат |
35
+ | ---------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
36
+ | `parseServiceMetadata(xmlText)` | Парсит OData V2 XML в `ServiceMetadata`: entities, properties, FunctionImports, keys, parameters и типы. Некорректный XML/metadata — ошибка boundary. |
37
+ | `resolveMetadataColumnLabel(column)` | Label с fallback на column id. |
38
+ | `collectFilterableColumns`, `collectFilterableColumnsIds` | Отбирают filterable metadata fields. |
39
+ | `wrapODataParams(params)`, `unwrapODataParams(params)` | Разделяют значения параметров и их OData type; используйте перед builder, а не сериализуйте вручную. |
40
+ | `buildParameterEntries`, `buildEntityParameters` | Строят key/predicate часть entity из metadata и wrapped params. |
41
+ | `buildEntityPath`, `buildEntityOperationPath` | Возвращают metadata-aware путь entity/operation. |
42
+ | `buildFunctionImportParameters`, `buildFunctionImportPath` | Строят FunctionImport URL/query через `URLSearchParams`. |
43
+
44
+ ```ts
45
+ const metadata = parseServiceMetadata(metadataXml);
46
+ const params = wrapODataParams({ id: "42" });
47
+ const path = buildEntityPath(entityMetadata, { service: "DEMO_SRV", target: "Items" }, params);
48
+ ```
49
+
50
+ Типы `ServiceMetadata`, `EntityMetadata`, `FunctionImportMetadata`, `EntityColumnProperty`, `ODataMetaType`, `WrappedODataParameters` и `ODataServiceConfig` экспортируются из того же subpath. Параметры entity и FunctionImport — разные OData contracts; не смешивайте их.
51
+
52
+ ### Значения и formatter registry
53
+
54
+ | API | Назначение |
55
+ | -------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
56
+ | `odataFormatValue(type, value)` | Сериализует primitive в OData literal с escaping/suffix/date semantics. |
57
+ | `odataParseValue(type, value)` | Возвращает primitive/date или исходное `unknown`, если значение не может быть безопасно преобразовано. |
58
+ | `odataParseValueByMetadata(descriptor, value)` | Парсит с учётом metadata descriptor, включая ABAP boolean-like поле. |
59
+ | `defaultODataTypeValue`, `defaultControlTypeValue` | Default value для OData/base control type. |
60
+ | `getBaseTypeFromODataType`, `isODataBooleanType`, `isODataNumericType`, `isODataIntegerType`, `isODataDateType` | Type classification. |
61
+ | `getFormattersFor`, `getFormattersForBaseType`, `getFormatter`, `getFormatterForBaseType` | Читают встроенный formatter descriptions по type/id. |
62
+ | `createFormatter`, `createBooleanFormatter`, `createStringFormatter`, `createNumberFormatter`, `createDateFormatter` | Создают typed formatter definitions с Zod input schema. |
63
+
64
+ ### Фильтры, sorts и SQL-like expressions
65
+
66
+ `FilterExpression<T>` — serializable дерево с `FilterConditionExpression` и `FilterNestedExpression`. Постройте его через `createFilter`, `createFilterEqual`, `createFilterEqualFalsy`, `createFilterContains`, `createFilterBetween`, объедините через `mergeFilterExpressions`, затем вызовите `buildODataFilter` или `buildODataFilterRecursive`.
67
+
68
+ ```ts
69
+ const expression = createFilterEqual<{ status: string }, string>("status", "active");
70
+ const filter = buildODataFilter(expression);
71
+ ```
72
+
73
+ | API | Контракт |
74
+ | ------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
75
+ | `FilterOperation`, `FILTER_OPERATIONS`, `isFilterOperation`, `resolveOperationLabel` | Стабильный список и validation operator-ов. |
76
+ | `Sort<T>`, `toggleSort`, `getSortIndex`, `getSortIndicator` | Immutable sort state для UI. |
77
+ | `buildODataOrder`, `resolveEffectiveSorts`, `buildODataOrderBy` | Сериализуют sort/grouping для OData. |
78
+ | `createSqlFilterEqual`, `createSqlFilterIn`, `createSqlFilterGroup`, `buildSqlFilter` | Отдельный SQL-filter contract: field id предварительно валидируется. Не подставляйте произвольный пользовательский field id. |
79
+
80
+ `ODataCompiledFilterDefinition`, `sanitizeFilterDefinitions`, `sanitizeFilterValues`, `mergeFilterValuePatch`, `compileFiltersToExpression`, `flattenFilterValuesToODataDependencies` предназначены для serializable UI filter configuration. Санитизация создаёт canonical configuration; не записывайте её поверх активного пользовательского draft без явно названной save/restore boundary.
81
+
82
+ ## `odata`: code/text collections
83
+
84
+ ```ts
85
+ import { buildSeparatedArrays, findCollectionPairs, ODataDateFormat } from "@ryuzaki13/react-foundation-lib/odata";
86
+ ```
87
+
88
+ | API | Назначение |
89
+ | ------------------------------------------------------ | ------------------------------------------------------------------------- |
90
+ | `buildSeparatedArrays(collection, ...)` | Разделяет OData collection на code/text arrays для select-like consumers. |
91
+ | `findCollectionPairs(...)` | Находит code/text pair по collection/dependency filters. |
92
+ | `ODataDateFormat` | Константы date formats для legacy OData values. |
93
+ | `CollectionItem`, `CollectionPair`, `DependencyFilter` | Публичные contracts коллекций. |
94
+
95
+ `useODataCollectionStore` и `odataCollectionConfig` описаны на странице Runtime/state: это UI defaults, а не OData transport/cache.
96
+
97
+ ## `table`: TanStack Table contracts
98
+
99
+ ```ts
100
+ import { buildTableColumnLayout, createTableColumnsFromODataMetadata, useTableRowSelection } from "@ryuzaki13/react-foundation-lib/table";
101
+ ```
102
+
103
+ `TableColumnDef<TData>` расширяет TanStack `ColumnDef`, а `TableColumnMeta`/`TableColumnFormattingMeta` описывают common metadata. Не мутируйте исходные `ColumnDef`: layout/order/sizing helpers возвращают новое состояние.
104
+
105
+ | Группа | Public API |
106
+ | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
107
+ | Layout | `resolveTableColumnOrder`, `buildTableColumnLayout`, `TableColumnLayoutItem`, `BuildTableColumnLayoutArgs`. |
108
+ | Order | `normalizeTableColumnOrder`, `reorderTableHeaderColumns`, `resolveReorderedTableHeaderColumns`, `TableColumnOrderState`. |
109
+ | Sizing | `DEFAULT_TABLE_COLUMN_MIN_WIDTH`, `normalizeTableColumnWidth`, `normalizeTableColumnSizing`, `patchTableColumnWidth`, `removeTableColumnWidth`, `TableColumnSizingState`. |
110
+ | Selection | `TableSelectionMode`, `toggleTableRowSelection`, `pruneTableRowSelection`, `useTableRowSelection`. |
111
+ | Metadata | `getTableColumnMeta`, `getTableColumnFormattingMeta`, `resolveTableLength`, `isTableInteractiveElement`. |
112
+
113
+ `useTableRowSelection` синхронизирует selection state с TanStack Table. Передавайте устойчивый `getRowId` на business identity, не array index: изменяемая сортировка/фильтрация иначе свяжет selection с неверной строкой.
114
+
115
+ ### Metadata-driven OData adapter
116
+
117
+ | API | Назначение |
118
+ | ------------------------------------------------------------ | ----------------------------------------------------------------------------------------- |
119
+ | `createTableColumnsFromODataMetadata(options)` | Строит `TableColumnDef` из OData metadata. |
120
+ | `enrichTableColumnsWithODataFormatting(columns, options)` | Добавляет typed formatting metadata к существующим columns без изменения OData transport. |
121
+ | `createTableColumnVisibilityFromODataMetadata(...)` | Формирует initial column visibility. |
122
+ | `resolveTableColumnFormattingContextFromODataColumn(column)` | Создаёт formatter context для одного metadata column. |
123
+ | `resolveStableColumnId(column)` | Определяет устойчивый id table column. |
124
+
125
+ `meta.formatting` — экспериментальный presentation contract, не OData request. Если pipeline требует дополнительные runtime fields, owner Query должен расширить select/dependencies через documented formatter pipeline API до fetch.
126
+
127
+ ## `tree-table`: построение дерева
128
+
129
+ ```ts
130
+ import { buildTreeTableRows, transposeFlatRowsToTreeTableRows } from "@ryuzaki13/react-foundation-lib/tree-table";
131
+ ```
132
+
133
+ | API | Контракт |
134
+ | ----------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------- |
135
+ | `buildTreeTableRows(rows, hierarchy)` | Строит `TreeTableBuildResult`: tree rows и diagnostics от flat hierarchy. |
136
+ | `transposeFlatRowsToTreeTableRows(rows, options)` | Создаёт display tree/синтетические nodes из flat rows. |
137
+ | `TreeTableFlatHierarchy`, `TreeTableRowNode`, `TreeTableTransposedRow`, `TreeTableTransposeFlatRowsOptions` | Публичные contracts и metadata fields. |
138
+
139
+ Функции не меняют исходные rows. Используйте explicit stable row/parent ids; не рассчитывайте на визуальный label как идентичность.
140
+
141
+ ## `formulas`: registry calculated columns
142
+
143
+ ```ts
144
+ import {
145
+ compileTableFormula,
146
+ configureTableFormulaRegistry,
147
+ createTableFormulaRegistry,
148
+ executeTableFormula,
149
+ validateTableFormulaDependencies
150
+ } from "@ryuzaki13/react-foundation-lib/formulas";
151
+ ```
152
+
153
+ ### Инициализация
154
+
155
+ Host владеет `TableFormulaDefinition[]` и один раз, до первого compile/validate/execute, создаёт и устанавливает registry:
156
+
157
+ ```ts
158
+ const registry = createTableFormulaRegistry([
159
+ {
160
+ id: "margin",
161
+ name: "Маржа",
162
+ description: "Выручка минус себестоимость",
163
+ fn: (ctx) => ctx.num(0) - ctx.num(1)
164
+ }
165
+ ]);
166
+ configureTableFormulaRegistry(registry);
167
+ ```
168
+
169
+ | API | Результат |
170
+ | ---------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- |
171
+ | `createTableFormulaContext({ keys, rowData })` | Контекст `key/value/num(index)` для formula function. |
172
+ | `createTableFormulaRegistry(definitions)` | Проверяет non-empty unique ids и создаёт immutable registry. |
173
+ | `getTableFormulaList`, `getTableFormulaById` | Читают активный registry. |
174
+ | `compileTableFormula({ formulaId, keys? })` | Компилирует executor или controlled compile result. |
175
+ | `executeTableFormula({ formulaId, keys?, rowData })` | Одноразово выполняет formula и возвращает `TableFormulaExecutionResult`, не выбрасывая formula runtime error наружу. |
176
+ | `validateTableFormulaDependencies(args)` | Проверяет formula id, dependency availability, out-of-range/unused indices. |
177
+
178
+ Formula function возвращает finite number. Ошибочные состояния (`formula_not_found`, `runtime_error`, `invalid_result`) являются данными результата, а не поводом silently подставить ноль. Обработайте их у formatter/table owner и показывайте безопасный fallback.