@rsconcept/rstool 0.5.2 → 0.6.1

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.
@@ -0,0 +1,371 @@
1
+ import type { BasicBinding } from '../../src';
2
+
3
+ /** Upper bound from axiom A1: `card(X1)≤10`. */
4
+ export const A1_MAX_PEOPLE = 10;
5
+
6
+ export type Gender = 'm' | 'f';
7
+
8
+ export type GenderRegistry = Record<string, Gender>;
9
+
10
+ /** Default gender for names in the sample kinship model. */
11
+ export const SAMPLE_GENDER: Readonly<Record<string, Gender>> = {
12
+ Иван: 'm',
13
+ Мария: 'f',
14
+ Пётр: 'm',
15
+ Петр: 'm',
16
+ Анна: 'f',
17
+ Олег: 'm',
18
+ Дарья: 'f',
19
+ Семён: 'm'
20
+ };
21
+
22
+ const GENDER_ALIASES: Readonly<Record<string, Gender>> = {
23
+ m: 'm',
24
+ male: 'm',
25
+ man: 'm',
26
+ м: 'm',
27
+ муж: 'm',
28
+ мужчина: 'm',
29
+ f: 'f',
30
+ female: 'f',
31
+ woman: 'f',
32
+ ж: 'f',
33
+ жен: 'f',
34
+ женщина: 'f'
35
+ };
36
+
37
+ /** Parses a gender token (`м`, `ж`, `m`, `f`, …). */
38
+ export function parseGenderToken(token: string): Gender | null {
39
+ return GENDER_ALIASES[token.trim().toLowerCase()] ?? null;
40
+ }
41
+
42
+ /** `add <пол> <имя>` — пол первым, имя может содержать пробелы. */
43
+ export function parseAddPersonArgs(args: string[]): { gender: Gender; name: string } {
44
+ if (args.length < 2) {
45
+ throw new Error('Укажите пол и имя: add <м|ж> <имя> (например: add м Олег)');
46
+ }
47
+ const gender = parseGenderToken(args[0]);
48
+ if (!gender) {
49
+ throw new Error('Первый аргумент — пол: м, ж, m или f');
50
+ }
51
+ const name = args.slice(1).join(' ').trim();
52
+ assertNonBlankName(name);
53
+ return { gender, name };
54
+ }
55
+
56
+ export function genderLabel(gender: Gender): string {
57
+ return gender === 'm' ? 'м' : 'ж';
58
+ }
59
+
60
+ /** Rebuilds the gender registry from S2/S3 base values and the current X1 binding. */
61
+ export function genderByNameFromSets(
62
+ binding: BasicBinding,
63
+ s2: readonly number[],
64
+ s3: readonly number[]
65
+ ): GenderRegistry {
66
+ const men = new Set(s2);
67
+ const women = new Set(s3);
68
+ const result: GenderRegistry = {};
69
+ for (const { index, name } of bindingEntries(binding)) {
70
+ if (men.has(index)) {
71
+ result[name] = 'm';
72
+ } else if (women.has(index)) {
73
+ result[name] = 'f';
74
+ }
75
+ }
76
+ return result;
77
+ }
78
+
79
+ /** Indices into X1 for S2 (мужчины) and S3 (женщины). */
80
+ export function deriveGenderSets(binding: BasicBinding, genderByName: Readonly<GenderRegistry>): {
81
+ s2: number[];
82
+ s3: number[];
83
+ } {
84
+ const s2: number[] = [];
85
+ const s3: number[] = [];
86
+ const missing: string[] = [];
87
+ for (const { index, name } of bindingEntries(binding)) {
88
+ const gender = genderByName[name];
89
+ if (gender === 'm') {
90
+ s2.push(index);
91
+ } else if (gender === 'f') {
92
+ s3.push(index);
93
+ } else {
94
+ missing.push(name);
95
+ }
96
+ }
97
+ if (missing.length > 0) {
98
+ throw new Error(`Не задан пол: ${missing.join(', ')}`);
99
+ }
100
+ return { s2, s3 };
101
+ }
102
+
103
+ /** Parent–child tuple: `[TUPLE_ID, parentIndex, childIndex]`. */
104
+ export type S1Value = number[][];
105
+
106
+ export function bindingEntries(binding: BasicBinding): { index: number; name: string }[] {
107
+ return Object.entries(binding)
108
+ .map(([key, name]) => ({ index: Number(key), name }))
109
+ .sort((left, right) => left.index - right.index);
110
+ }
111
+
112
+ export function formatX1(binding: BasicBinding, genderByName?: Readonly<GenderRegistry>): string {
113
+ const entries = bindingEntries(binding);
114
+ if (entries.length === 0) {
115
+ return '(пусто)';
116
+ }
117
+ return entries
118
+ .map(entry => {
119
+ const gender = genderByName?.[entry.name];
120
+ const suffix = gender ? ` (${genderLabel(gender)})` : '';
121
+ return `${entry.index}: ${entry.name}${suffix}`;
122
+ })
123
+ .join('\n');
124
+ }
125
+
126
+ export function formatS1(binding: BasicBinding, s1: S1Value): string {
127
+ if (s1.length === 0) {
128
+ return '(пусто)';
129
+ }
130
+ return s1
131
+ .map(tuple => {
132
+ const parent = binding[tuple[1]] ?? `#${tuple[1]}`;
133
+ const child = binding[tuple[2]] ?? `#${tuple[2]}`;
134
+ return `${parent} → ${child}`;
135
+ })
136
+ .join('\n');
137
+ }
138
+
139
+ export function findIndexByName(binding: BasicBinding, name: string): number | null {
140
+ for (const [key, value] of Object.entries(binding)) {
141
+ if (value === name) {
142
+ return Number(key);
143
+ }
144
+ }
145
+ return null;
146
+ }
147
+
148
+ export function listNames(binding: BasicBinding): string[] {
149
+ return bindingEntries(binding).map(entry => entry.name);
150
+ }
151
+
152
+ /** `card(X1)` for the current binding. */
153
+ export function x1Cardinality(binding: BasicBinding): number {
154
+ return bindingEntries(binding).length;
155
+ }
156
+
157
+ /** Whether binding satisfies A1 (`card(X1)≤10`). */
158
+ export function satisfiesA1MaxPeople(binding: BasicBinding, maxPeople = A1_MAX_PEOPLE): boolean {
159
+ return x1Cardinality(binding) <= maxPeople;
160
+ }
161
+
162
+ export function formatA1Status(binding: BasicBinding, maxPeople = A1_MAX_PEOPLE): string {
163
+ const count = x1Cardinality(binding);
164
+ const holds = count <= maxPeople;
165
+ return `A1 card(X1)≤${maxPeople}: ${holds ? 'выполняется' : 'нарушена'} (${count} чел.)`;
166
+ }
167
+
168
+ function assertA1MaxPeople(binding: BasicBinding): void {
169
+ const count = x1Cardinality(binding);
170
+ if (count > A1_MAX_PEOPLE) {
171
+ throw new Error(
172
+ `Аксиома A1 «card(X1)≤${A1_MAX_PEOPLE}» нарушена: ${count} человек (максимум ${A1_MAX_PEOPLE})`
173
+ );
174
+ }
175
+ }
176
+
177
+ function assertUniqueName(binding: BasicBinding, name: string): void {
178
+ if (findIndexByName(binding, name) !== null) {
179
+ throw new Error(`Человек «${name}» уже есть в X1`);
180
+ }
181
+ }
182
+
183
+ function assertPersonExists(binding: BasicBinding, name: string): number {
184
+ const index = findIndexByName(binding, name);
185
+ if (index === null) {
186
+ throw new Error(`Человек «${name}» не найден в X1 (доступны: ${listNames(binding).join(', ') || '—'})`);
187
+ }
188
+ return index;
189
+ }
190
+
191
+ function assertNonBlankName(name: string): void {
192
+ if (!name.trim()) {
193
+ throw new Error('Имя не может быть пустым');
194
+ }
195
+ }
196
+
197
+ export function addPerson(
198
+ binding: BasicBinding,
199
+ name: string,
200
+ gender: Gender,
201
+ genderByName: GenderRegistry
202
+ ): BasicBinding {
203
+ assertNonBlankName(name);
204
+ assertUniqueName(binding, name);
205
+ const indices = Object.keys(binding).map(Number);
206
+ const nextIndex = indices.length === 0 ? 0 : Math.max(...indices) + 1;
207
+ const nextBinding = { ...binding, [nextIndex]: name };
208
+ assertA1MaxPeople(nextBinding);
209
+ genderByName[name] = gender;
210
+ return nextBinding;
211
+ }
212
+
213
+ export function renamePerson(
214
+ binding: BasicBinding,
215
+ oldName: string,
216
+ newName: string,
217
+ genderByName: GenderRegistry
218
+ ): BasicBinding {
219
+ assertNonBlankName(oldName);
220
+ assertNonBlankName(newName);
221
+ const index = assertPersonExists(binding, oldName);
222
+ if (oldName !== newName) {
223
+ assertUniqueName(binding, newName);
224
+ const gender = genderByName[oldName];
225
+ if (gender) {
226
+ delete genderByName[oldName];
227
+ genderByName[newName] = gender;
228
+ }
229
+ }
230
+ return { ...binding, [index]: newName };
231
+ }
232
+
233
+ export function setX1List(names: string[]): BasicBinding {
234
+ const seen = new Set<string>();
235
+ const binding: BasicBinding = {};
236
+ for (const name of names) {
237
+ assertNonBlankName(name);
238
+ if (seen.has(name)) {
239
+ throw new Error(`Дубликат имени: «${name}»`);
240
+ }
241
+ seen.add(name);
242
+ binding[Object.keys(binding).length] = name;
243
+ }
244
+ assertA1MaxPeople(binding);
245
+ return binding;
246
+ }
247
+
248
+ /** Replaces X1; each person is `пол` then `имя` (имя до следующего пола или конца). */
249
+ export function setX1WithGender(specs: { gender: Gender; name: string }[]): {
250
+ binding: BasicBinding;
251
+ genderByName: GenderRegistry;
252
+ } {
253
+ const genderByName: GenderRegistry = {};
254
+ const binding: BasicBinding = {};
255
+ const seen = new Set<string>();
256
+ for (const { gender, name } of specs) {
257
+ assertNonBlankName(name);
258
+ if (seen.has(name)) {
259
+ throw new Error(`Дубликат имени: «${name}»`);
260
+ }
261
+ seen.add(name);
262
+ binding[Object.keys(binding).length] = name;
263
+ genderByName[name] = gender;
264
+ }
265
+ assertA1MaxPeople(binding);
266
+ return { binding, genderByName };
267
+ }
268
+
269
+ /** Parses `set м Иван ж Мария` or legacy `set Иван Мария` (пол из реестра / SAMPLE_GENDER). */
270
+ export function parseSetPersonArgs(
271
+ args: string[],
272
+ existingGenderByName: Readonly<GenderRegistry>
273
+ ): { specs: { gender: Gender; name: string }[]; legacyNamesOnly: boolean } {
274
+ if (args.length === 0) {
275
+ throw new Error('Укажите людей: set <м|ж> <имя> … или set <имя1> <имя2> …');
276
+ }
277
+ if (parseGenderToken(args[0]) !== null) {
278
+ const specs: { gender: Gender; name: string }[] = [];
279
+ let index = 0;
280
+ while (index < args.length) {
281
+ const gender = parseGenderToken(args[index]);
282
+ if (!gender) {
283
+ throw new Error(`Ожидался пол (м/ж), получено: «${args[index]}»`);
284
+ }
285
+ const nameParts: string[] = [];
286
+ index += 1;
287
+ while (index < args.length && parseGenderToken(args[index]) === null) {
288
+ nameParts.push(args[index]);
289
+ index += 1;
290
+ }
291
+ const name = nameParts.join(' ').trim();
292
+ if (!name) {
293
+ throw new Error(`После пола «${genderLabel(gender)}» укажите имя`);
294
+ }
295
+ specs.push({ gender, name });
296
+ }
297
+ return { specs, legacyNamesOnly: false };
298
+ }
299
+ const legacyNames = args.map(name => name.trim()).filter(Boolean);
300
+ const specs = legacyNames.map(name => {
301
+ const gender = existingGenderByName[name] ?? SAMPLE_GENDER[name];
302
+ if (!gender) {
303
+ throw new Error(
304
+ `Для «${name}» не задан пол. Используйте: set <м|ж> <имя> … (например: set м ${name})`
305
+ );
306
+ }
307
+ return { gender, name };
308
+ });
309
+ return { specs, legacyNamesOnly: true };
310
+ }
311
+
312
+ export function clearX1Binding(): BasicBinding {
313
+ return {};
314
+ }
315
+
316
+ function reindexBindingAfterRemove(binding: BasicBinding, removedIndex: number): BasicBinding {
317
+ const result: BasicBinding = {};
318
+ for (const [key, name] of Object.entries(binding)) {
319
+ const index = Number(key);
320
+ if (index === removedIndex) {
321
+ continue;
322
+ }
323
+ result[index > removedIndex ? index - 1 : index] = name;
324
+ }
325
+ return result;
326
+ }
327
+
328
+ export function remapS1AfterRemove(s1: S1Value, removedIndex: number): S1Value {
329
+ return s1
330
+ .filter(tuple => tuple[1] !== removedIndex && tuple[2] !== removedIndex)
331
+ .map(tuple => [
332
+ tuple[0],
333
+ tuple[1] > removedIndex ? tuple[1] - 1 : tuple[1],
334
+ tuple[2] > removedIndex ? tuple[2] - 1 : tuple[2]
335
+ ]);
336
+ }
337
+
338
+ export function remapS1ByNames(oldBinding: BasicBinding, newBinding: BasicBinding, s1: S1Value): S1Value {
339
+ const newIndexByName = new Map(bindingEntries(newBinding).map(entry => [entry.name, entry.index]));
340
+ const result: S1Value = [];
341
+
342
+ for (const tuple of s1) {
343
+ const parentName = oldBinding[tuple[1]];
344
+ const childName = oldBinding[tuple[2]];
345
+ if (!parentName || !childName) {
346
+ continue;
347
+ }
348
+ const parentIndex = newIndexByName.get(parentName);
349
+ const childIndex = newIndexByName.get(childName);
350
+ if (parentIndex === undefined || childIndex === undefined) {
351
+ continue;
352
+ }
353
+ result.push([tuple[0], parentIndex, childIndex]);
354
+ }
355
+
356
+ return result;
357
+ }
358
+
359
+ export function removePerson(
360
+ binding: BasicBinding,
361
+ s1: S1Value,
362
+ name: string,
363
+ genderByName: GenderRegistry
364
+ ): { binding: BasicBinding; s1: S1Value } {
365
+ const removedIndex = assertPersonExists(binding, name);
366
+ delete genderByName[name];
367
+ return {
368
+ binding: reindexBindingAfterRemove(binding, removedIndex),
369
+ s1: remapS1AfterRemove(s1, removedIndex)
370
+ };
371
+ }