Forge
csharpdeeb25a2
1#nullable enable
2
3namespace CascadeIDE.Features.Chat;
4
5/// <summary>
6/// Парсер multi-segment хвостов <c>[a;b] [c;d]</c> (ADR 0138) с legacy contiguous <c>3:5</c>, <c>3 5</c>, <c>5</c>.
7/// </summary>
8public static class ParametricSegmentListParser
9{
10 public static bool TryParse(
11 string? argsTail,
12 out IReadOnlyList<ParametricIntRange> segments,
13 out string error)
14 {
15 segments = [];
16 error = "";
17
18 var tail = (argsTail ?? "").Trim();
19 if (tail.Length == 0)
20 {
21 error = "Укажи диапазон: «5», «5 10», «5:10» или «[3;5] [8;15] [20]».";
22 return false;
23 }
24
25 if (tail.Contains('['))
26 return TryParseBracketSegments(tail, out segments, out error);
27
28 if (!ChatSlashParametricArgsBuilder.TryParseLineRangeTail(tail, out var start, out var end, out error))
29 return false;
30
31 segments = [new ParametricIntRange(start, end)];
32 return true;
33 }
34
35 public static bool TryParseSingleContiguous(
36 string? argsTail,
37 out ParametricIntRange range,
38 out string error)
39 {
40 range = default;
41 if (!TryParse(argsTail, out var segments, out error))
42 return false;
43
44 if (segments.Count != 1)
45 {
46 error = segments.Count == 0
47 ? "Пустой диапазон."
48 : "Ожидается один contiguous сегмент (например 3:5). Для disjoint используйте [3;5] [8;15].";
49 return false;
50 }
51
52 range = segments[0];
53 return true;
54 }
55
56 public static string FormatSummary(IReadOnlyList<ParametricIntRange> segments, string unitLabel)
57 {
58 if (segments.Count == 0)
59 return "";
60
61 var parts = new List<string>(segments.Count);
62 var total = 0;
63 foreach (var segment in segments)
64 {
65 total += segment.InclusiveCount;
66 parts.Add(segment.Start == segment.End
67 ? $"{segment.Start}"
68 : $"{segment.Start}–{segment.End}");
69 }
70
71 var joined = string.Join(", ", parts);
72 return segments.Count == 1 && segments[0].Start == segments[0].End
73 ? $"{unitLabel}: {joined}"
74 : $"{unitLabel}: {joined} ({total} {PluralUnit(total, unitLabel)})";
75 }
76
77 private static string PluralUnit(int count, string unitLabel)
78 {
79 var key = unitLabel.Trim().ToLowerInvariant();
80 return key switch
81 {
82 "строки" or "строка" => count == 1 ? "строка" : "строк",
83 "сообщения" or "сообщение" => count == 1 ? "сообщение" : "сообщений",
84 _ => count == 1 ? unitLabel.TrimEnd('и') : unitLabel,
85 };
86 }
87
88 private static bool TryParseBracketSegments(
89 string tail,
90 out IReadOnlyList<ParametricIntRange> segments,
91 out string error)
92 {
93 var list = new List<ParametricIntRange>();
94 var i = 0;
95 while (i < tail.Length)
96 {
97 while (i < tail.Length && char.IsWhiteSpace(tail[i]))
98 i++;
99
100 if (i >= tail.Length)
101 break;
102
103 if (tail[i] != '[')
104 {
105 error = "Сегменты disjoint задаются только в скобках: [3;5] [8;15]. Legacy «3:5» — без скобок.";
106 segments = [];
107 return false;
108 }
109
110 var close = tail.IndexOf(']', i + 1);
111 if (close < 0)
112 {
113 error = "Незакрытая «[» в сегменте.";
114 segments = [];
115 return false;
116 }
117
118 var inner = tail[(i + 1)..close].Trim();
119 if (!TryParseBracketInner(inner, out var range, out error))
120 {
121 segments = [];
122 return false;
123 }
124
125 list.Add(range);
126 i = close + 1;
127 }
128
129 if (list.Count == 0)
130 {
131 error = "Нет сегментов в скобках.";
132 segments = [];
133 return false;
134 }
135
136 segments = list;
137 error = "";
138 return true;
139 }
140
141 private static bool TryParseBracketInner(string inner, out ParametricIntRange range, out string error)
142 {
143 range = default;
144 error = "";
145
146 if (inner.Length == 0)
147 {
148 error = "Пустой сегмент [].";
149 return false;
150 }
151
152 var parts = inner.Split(';', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
153 if (parts.Length is < 1 or > 2)
154 {
155 error = "Внутри [] ожидается одно число [20] или пара [3;5].";
156 return false;
157 }
158
159 if (!int.TryParse(parts[0], out var start) || start < 1)
160 {
161 error = $"Некорректное число «{parts[0]}».";
162 return false;
163 }
164
165 if (parts.Length == 1)
166 {
167 range = new ParametricIntRange(start, start);
168 return true;
169 }
170
171 if (!int.TryParse(parts[1], out var end) || end < 1)
172 {
173 error = $"Некорректное число «{parts[1]}».";
174 return false;
175 }
176
177 if (end < start)
178 {
179 error = "Конец сегмента не может быть меньше начала.";
180 return false;
181 }
182
183 range = new ParametricIntRange(start, end);
184 return true;
185 }
186}
187
View only · write via MCP/CIDE