windowcapture
исходный код / Helpers/RulesEngine.cs

RulesEngine.cs

667 строк · 37,070 байт · модуль Helpers
  1using System;
  2using System.Collections.Generic;
  3using System.Text;
  4
  5namespace WindowCapture.Helpers
  6{
  7    /// <summary>
  8    /// RulesEngine v2: Complete Russian language rules.
  9    /// Auto-generates corrections from morphology, phonetics, grammar.
 10    /// Sources: orthographia.ru, Rosenthal's guide, academic rules 1956+2006.
 11    /// </summary>
 12    public static class RulesEngine
 13    {
 14        private static Dictionary<string, string> corrections;
 15        private static volatile bool ready;
 16        public static bool IsReady { get { return ready; } }
 17
 18        // ===== Word classification sets =====
 19        public static readonly HashSet<string> SubordinateConj = new HashSet<string> {
 20            "что","чтобы","который","которая","которое","которые","которого","которой","которому",
 21            "которым","которых","когда","где","куда","откуда","если","хотя","пока","чем",
 22            "поскольку","ибо","пока","раз","коли","дабы","ежели","как"
 23        };
 24        public static readonly HashSet<string> CoordinateConj = new HashSet<string> {
 25            "но","однако","зато","а"
 26        };
 27        public static readonly HashSet<string> IntroWords = new HashSet<string> {
 28            "конечно","наверное","видимо","кажется","пожалуй","например",
 29            "короче","впрочем","кстати","значит","следовательно","итак",
 30            "правда","честно","собственно","допустим","предположим",
 31            "действительно","естественно","очевидно","несомненно",
 32            "безусловно","разумеется","вероятно","возможно","наконец",
 33        };
 34        public static readonly HashSet<string> Prepositions = new HashSet<string> {
 35            "в","на","по","к","с","у","за","от","из","до","для","без","при","через",
 36            "между","под","над","перед","про","обо","ко","со","во"
 37        };
 38
 39        // Предлоги и падежи: какой падеж требует предлог
 40        // Р=родительный, Д=дательный, В=винительный, Т=творительный, П=предложный
 41        private static readonly Dictionary<string, string> prepCase = new Dictionary<string, string> {
 42            {"от","Р"},{"до","Р"},{"из","Р"},{"без","Р"},{"у","Р"},{"для","Р"},{"около","Р"},{"после","Р"},{"кроме","Р"},
 43            {"к","Д"},{"по","Д"},
 44            {"в","ВП"},{"на","ВП"},{"за","ВТ"},{"про","В"},{"через","В"},
 45            {"с","РТ"},{"со","РТ"},{"под","ВТ"},{"над","Т"},{"между","Т"},{"перед","Т"},
 46            {"о","П"},{"об","П"},{"при","П"},
 47        };
 48
 49        // Infinitive context markers (prevWord → next verb should be -ться)
 50        private static readonly HashSet<string> infinitiveMarkers = new HashSet<string> {
 51            "надо","нужно","можно","нельзя","хочу","хочет","хочешь","хотят",
 52            "буду","будет","будут","будешь","будем","стоит","стану","станет",
 53            "могу","может","можешь","могут","должен","должна","должны",
 54            "пора","лучше","готов","готова","собираюсь","решил","решила",
 55            "начал","начала","начну","перестал","перестану","попробую","попробовал"
 56        };
 57
 58        // ===== Phonetic maps =====
 59        private static readonly char[][] vowelPairs = {
 60            new[]{'о','а'}, new[]{'а','о'}, new[]{'е','и'}, new[]{'и','е'}, new[]{'е','я'}, new[]{'я','е'}
 61        };
 62        private static readonly Dictionary<char, char> voicedToVoiceless = new Dictionary<char, char> {
 63            {'б','п'},{'в','ф'},{'г','к'},{'д','т'},{'ж','ш'},{'з','с'}
 64        };
 65
 66        // ===== Verb ending patterns =====
 67        private static readonly string[] verbEndings2nd = { "ишь","ит","им","ите","ат","ят" }; // 2 спряжение
 68        private static readonly string[] verbEndings1st = { "ешь","ет","ем","ете","ут","ют" }; // 1 спряжение
 69
 70        // ===== Build =====
 71        public static void Build(string[] trustedWords, BloomFilter bloom)
 72        {
 73            var sw = System.Diagnostics.Stopwatch.StartNew();
 74            corrections = new Dictionary<string, string>(StringComparer.Ordinal);
 75
 76            // Phase 1: Manual (highest priority, overrides auto-generated)
 77            AddManualCorrections();
 78            int manual = corrections.Count;
 79
 80            // Phase 2: Auto-generate from trusted words
 81            int gen = 0;
 82            if (trustedWords != null)
 83            {
 84                int limit = Math.Min(trustedWords.Length, 40000);
 85                for (int i = 0; i < limit; i++)
 86                {
 87                    string w = trustedWords[i];
 88                    if (w.Length < 3 || w.Length > 15) continue;
 89                    gen += GenPhoneticErrors(w);
 90                    gen += GenMissingSoftSign(w);
 91                    gen += GenDoubleConsonant(w);
 92                    gen += GenEndVoicing(w);
 93                    gen += GenTsyaErrors(w);
 94                    gen += GenMissingSh(w);
 95                    gen += GenDoubleVowelDrop(w);
 96                    gen += GenUnpronouncedConsonant(w); // "солнце"→"сонце"
 97                    gen += GenYoAfterSh(w);             // "шёпот"→"шопот"
 98                    gen += GenHardSoftSign(w);           // "объём"→"обем"
 99                    gen += GenShSoftSign(w);             // "мышь"→"мыш"
100                    gen += GenPrefixZS(w);              // "разбить"↔"расбить"
101                }
102            }
103
104            sw.Stop();
105            Logger.Log("textproc", "RulesEngine v2: " + corrections.Count + " total (" + manual + " manual + " + gen + " generated) in " + sw.ElapsedMilliseconds + "ms");
106            ready = true;
107        }
108
109        // ===== Lookup =====
110        public static string Lookup(string lower)
111        {
112            if (!ready || corrections == null) return null;
113            string r; return corrections.TryGetValue(lower, out r) ? r : null;
114        }
115
116        // ===== -тся/-ться contextual =====
117        public static string FixTsyaTsya(string word, string prevWord)
118        {
119            string lower = word.ToLower();
120            if (lower.EndsWith("ца") && lower.Length >= 4)
121            {
122                string stem = lower.Substring(0, lower.Length - 2);
123                return infinitiveMarkers.Contains(prevWord.ToLower()) ? stem + "ться" : stem + "тся";
124            }
125            if (lower.EndsWith("цца") && lower.Length >= 5)
126            {
127                string stem = lower.Substring(0, lower.Length - 3);
128                return infinitiveMarkers.Contains(prevWord.ToLower()) ? stem + "ться" : stem + "тся";
129            }
130            return null;
131        }
132
133        // ===== Context-aware word check =====
134        /// <summary>Check if word's ending agrees with previous word's grammar.</summary>
135        public static string FixAgreement(string word, string prevWord, CompactSpell spell)
136        {
137            if (spell == null || !spell.IsReady) return null;
138            string lower = word.ToLower();
139            string prevLow = prevWord.ToLower();
140
141            // After preposition → check correct case ending
142            string reqCase;
143            if (prepCase.TryGetValue(prevLow, out reqCase))
144            {
145                // If word not in dict → maybe wrong ending
146                if (!spell.ContainsExact(lower) && lower.Length >= 4)
147                {
148                    // Try common ending substitutions
149                    string[] endings = GetExpectedEndings(reqCase);
150                    if (endings != null)
151                    {
152                        foreach (string end in endings)
153                        {
154                            // Try replacing last 1-3 chars with expected ending
155                            for (int cut = 1; cut <= Math.Min(3, lower.Length - 2); cut++)
156                            {
157                                string candidate = lower.Substring(0, lower.Length - cut) + end;
158                                if (spell.ContainsExact(candidate) || spell.ContainsTrusted(candidate))
159                                    return candidate;
160                            }
161                        }
162                    }
163                }
164            }
165            return null;
166        }
167
168        private static string[] GetExpectedEndings(string caseCode)
169        {
170            // Common endings for each case
171            if (caseCode.Contains("Р")) return new[]{"а","я","ы","и","ов","ей","ий"};
172            if (caseCode.Contains("Д")) return new[]{"у","ю","е","и","ам","ям"};
173            if (caseCode.Contains("В")) return new[]{"а","я","у","ю","о","е",""};
174            if (caseCode.Contains("Т")) return new[]{"ом","ем","ой","ей","ью","ами","ями"};
175            if (caseCode.Contains("П")) return new[]{"е","и","у","ю","ах","ях"};
176            return null;
177        }
178
179        // ===== Punctuation =====
180        public static bool NeedsCommaBefore(string word, string prevWord, bool hasPunctBefore)
181        {
182            if (hasPunctBefore) return false;
183            string w = word.ToLower();
184            string p = prevWord.ToLower();
185
186            // "потому что" — comma before "потому", not "что"
187            if (w == "что" && p == "потому") return false;
188            if (w == "потому") return true;
189            // "так как" — comma before "так", not "как"
190            if (w == "как" && (p == "так" || p == "такой" || p == "такая" || p == "такие")) return false;
191
192            if (SubordinateConj.Contains(w)) return true;
193            if (CoordinateConj.Contains(w)) return true;
194            if (IntroWords.Contains(w)) return true;
195            return false;
196        }
197
198        public static bool NeedsCommaAfter(string word)
199        {
200            return IntroWords.Contains(word.ToLower());
201        }
202
203        // ===== Merge rules =====
204        private static readonly HashSet<string> mergePrefixes = new HashSet<string> {
205            "по","за","вы","от","об","при","пере","под","над","про","до","раз","рас",
206            "видео","аудио","авто","само","маркет","капс","кар","воз","нейро","неро"
207        };
208
209        public static bool IsMergePrefix(string word)
210        {
211            return mergePrefixes.Contains(word.ToLower());
212        }
213
214        // ===== Dashes =====
215        /// <summary>Check if dash needed between two words: "Жизнь — боль"</summary>
216        public static bool NeedsDash(string word1, string word2)
217        {
218            // Both should be nouns in nominative case (rough check)
219            string w1 = word1.ToLower(), w2 = word2.ToLower();
220            if (w1.Length < 3 || w2.Length < 3) return false;
221            // Not verbs, not adjectives
222            bool w1Noun = !EndsWith(w1, "ть","ет","ит","ал","ла","ый","ий","ая","ое","ые");
223            bool w2Noun = !EndsWith(w2, "ть","ет","ит","ал","ла","ый","ий","ая","ое","ые");
224            // Both nominative-looking
225            bool w1Nom = EndsWith(w1, "ь","а","о","е","к","г","н","т","д","р","й");
226            bool w2Nom = EndsWith(w2, "ь","а","о","е","к","г","н","т","д","р","й");
227            return w1Noun && w2Noun && w1Nom && w2Nom;
228        }
229
230        // ===== Postfix rules (-то, -либо, -нибудь, кое-) =====
231        public static string FixPostfixes(string text)
232        {
233            string[] postfixes = { "то", "либо", "нибудь" };
234            string[] pronouns = { "как","какой","какая","какое","какие","что","кто","где","куда","когда","откуда","почему","зачем","сколько","чей","чья","чьё" };
235
236            foreach (string pfx in postfixes)
237            {
238                foreach (string pro in pronouns)
239                {
240                    string merged = pro + pfx;
241                    string correct = pro + "-" + pfx;
242                    int idx = 0;
243                    while ((idx = text.ToLower().IndexOf(merged, idx)) >= 0)
244                    {
245                        bool leftOk = idx == 0 || !char.IsLetter(text[idx - 1]);
246                        bool rightOk = idx + merged.Length >= text.Length || !char.IsLetter(text[idx + merged.Length]);
247                        if (leftOk && rightOk)
248                        {
249                            string orig = text.Substring(idx, merged.Length);
250                            string repl = orig.Substring(0, pro.Length) + "-" + orig.Substring(pro.Length);
251                            text = text.Substring(0, idx) + repl + text.Substring(idx + merged.Length);
252                            idx += repl.Length;
253                        }
254                        else idx++;
255                    }
256                }
257            }
258
259            // кое- prefix
260            string[] koeWords = { "что","кто","где","куда","как","какой","какая","какие" };
261            foreach (string kw in koeWords)
262            {
263                string merged = "кое" + kw;
264                string correct = "кое-" + kw;
265                text = ReplaceWholeWord(text, merged, correct);
266            }
267
268            return text;
269        }
270
271        // ===== Generation methods =====
272
273        private static void AddManualCorrections()
274        {
275            // TOP PHONETIC ERRORS (как слышится)
276            A("вобще","вообще");A("вобщем","в общем");A("кароче","короче");
277            A("миня","меня");A("тибя","тебя");A("сибя","себя");A("сибе","себе");A("мене","меня");
278            A("шол","шёл");A("пришол","пришёл");A("ушол","ушёл");A("пошол","пошёл");A("нашол","нашёл");
279            A("щас","сейчас");A("здрасти","здравствуйте");A("здрасте","здравствуйте");
280            A("сдесь","здесь");A("зделал","сделал");A("зделать","сделать");A("зделано","сделано");
281            A("придти","прийти");A("прити","прийти");A("притти","прийти");
282            A("ихний","их");A("евоный","его");A("ево","его");
283            A("канешна","конечно");A("канешно","конечно");
284            A("харашо","хорошо");A("хараша","хороша");
285            A("наверна","наверное");A("наверно","наверное");
286            A("пажалуста","пожалуйста");A("пожалуста","пожалуйста");
287            A("званить","звонить");A("званок","звонок");A("званю","звоню");
288            A("нисет","несёт");A("нису","несу");
289            A("чюш","чушь");A("чюшь","чушь");
290            A("канцов","концов");A("вконце","в конце");
291            A("превез","привёз");A("превезли","привезли");
292            A("прекиньте","прикиньте");A("прекинь","прикинь");
293            A("одрису","адресу");A("одрис","адрес");
294            A("средстф","средств");A("средстр","средств");
295            A("напесал","написал");A("напесать","написать");
296            A("выносима","выносимо");A("невыносима","невыносимо");
297            A("проста","просто");A("фподдержку","в поддержку");
298            A("штобы","чтобы");A("штоб","чтоб");A("што","что");A("шо","что");
299            A("новы","новый");A("стары","старый");
300            A("типерь","теперь");A("ришил","решил");A("ришать","решать");
301            A("полною","полную");A("жизь","жизнь");A("роликав","роликов");
302            A("каторый","который");A("каторая","которая");A("каторое","которое");
303            A("можэт","может");A("чесло","число");A("прешлось","пришлось");
304            A("етому","этому");A("тоге","итоге");A("втоге","в итоге");
305            A("видио","видео");A("ришыл","решил");A("решыл","решил");
306
307            // ДВОЙНЫЕ СОГЛАСНЫЕ
308            A("агенство","агентство");A("агенства","агентства");
309            A("учавствовать","участвовать");A("учавствую","участвую");
310            A("програма","программа");A("програмы","программы");A("програму","программу");
311            A("каллега","коллега");A("колега","коллега");
312            A("коментарий","комментарий");A("каммент","комментарий");
313            A("граммотный","грамотный");A("граммотность","грамотность");
314            A("оффициальный","официальный");A("оффис","офис");
315            A("металический","металлический");A("металическая","металлическая");
316            A("искуственый","искусственный");A("искуственая","искусственная");
317            A("рассыпаный","рассыпанный");
318            A("расчитать","рассчитать");A("расчитывал","рассчитывал");A("расчитывать","рассчитывать");
319
320            // ПРИСТАВКИ ПРЕ-/ПРИ-
321            A("привецтвую","приветствую");A("приветствую","приветствую");
322            A("преодалевать","преодолевать");A("преадолевать","преодолевать");
323            A("симпотичный","симпатичный");A("координально","кардинально");
324            A("будующий","будущий");A("следущий","следующий");
325
326            // ГЛАГОЛЫ без Ь
327            A("шариш","шаришь");A("знаеш","знаешь");A("хочеш","хочешь");A("можеш","можешь");
328            A("делаеш","делаешь");A("думаеш","думаешь");A("понимаеш","понимаешь");
329            A("говориш","говоришь");A("видиш","видишь");A("слышиш","слышишь");
330            A("пишеш","пишешь");A("читаеш","читаешь");A("идеш","идёшь");
331
332            // СЛОЖНЫЕ DIST>2 ОШИБКИ
333            A("патамушта","потому что");A("патамучта","потому что");
334            A("тихналогиях","технологиях");A("тихналогия","технология");A("тихнология","технология");
335            A("рендаренга","рендеринга");A("рендаренг","рендеринг");
336            A("деминистратора","администратора");A("деминистратор","администратор");
337            A("компьютор","компьютер");A("компутер","компьютер");A("компуктер","компьютер");A("компуктером","компьютером");
338            A("интирнет","интернет");A("росия","россия");A("помошник","помощник");
339            A("поциент","пациент");A("извените","извините");
340            A("дратути","здравствуйте");A("пылисосить","пылесосить");A("пылисос","пылесос");
341            A("марочился","морочился");A("понел","понял");A("понела","поняла");
342            A("нужнали","нужна ли");
343
344            // ОКОНЧАНИЯ + ПАДЕЖИ
345            A("плокат","плакат");A("таго","того");
346            A("зделке","сделке");A("зделку","сделку");A("зделка","сделка");
347            A("пожаловаца","пожаловаться");A("завышеной","завышенной");
348            A("некомпентно","некомпетентно");A("компентно","компетентно");
349            A("рукаводитель","руководитель");A("предлажил","предложил");
350            A("заключять","заключать");
351            A("алюминевый","алюминиевый");A("професиональный","профессиональный");
352            A("почуствовал","почувствовал");
353            A("нечяено","нечаянно");A("нечаяно","нечаянно");
354            A("раскажу","расскажу");A("машыну","машину");A("машына","машина");
355            A("хорошева","хорошего");A("кажеца","кажется");
356            A("краце","вкратце");A("вкраце","вкратце");A("задорага","задорого");
357
358            // COMPOUND + MODERN
359            A("неросеть","нейросеть");A("неросети","нейросети");
360            A("разгаваривает","разговаривает");A("гаваривает","говорит");
361            A("раззлился","разозлился");
362            A("какойто","какой-то");A("какоето","какое-то");A("какието","какие-то");
363            A("чтото","что-то");A("гдето","где-то");A("кудато","куда-то");
364            A("ктото","кто-то");A("когдато","когда-то");
365            A("банкофскую","банковскую");A("банкофский","банковский");
366            A("маркетплейс","маркетплейс");A("маркетплейсе","маркетплейсе");
367            A("капслоком","капслоком");A("капслок","капслок");
368            A("падругому","по другому");
369            A("здали","сдали");A("здать","сдать");A("здал","сдал");
370            A("праграмму","программу");A("праграмма","программа");A("праграммы","программы");
371            A("щитать","считать");A("щитаю","считаю");
372            A("серьозно","серьёзно");A("серьозный","серьёзный");
373
374            // НЕПРОИЗНОСИМЫЕ СОГЛАСНЫЕ
375            A("сонце","солнце");A("серце","сердце");A("лесница","лестница");
376            A("чуство","чувство");A("чуствую","чувствую");A("чуствовал","чувствовал");
377            A("учаснник","участник");A("учасник","участник");
378            A("ровесник","ровесник");// correct
379            A("празник","праздник");A("празднек","праздник");
380            A("здраствуй","здравствуй");A("здраствуйте","здравствуйте");
381
382            // Ё ПОСЛЕ ШИПЯЩИХ
383            A("шопот","шёпот");A("жолудь","жёлудь");A("чорный","чёрный");
384            A("пчолка","пчёлка");A("жолтый","жёлтый");A("щотка","щётка");
385            A("шолк","шёлк");A("печонка","печёнка");
386
387            // Ъ/Ь РАЗДЕЛИТЕЛЬНЫЕ
388            A("обем","объём");A("семка","съёмка");A("подезд","подъезд");
389            A("обявление","объявление");A("обяснить","объяснить");A("обяснение","объяснение");
390            A("сехал","съехал");A("сел","съел");
391
392            // Ь ПОСЛЕ ШИПЯЩИХ (3 склонение)
393            A("мыш","мышь");A("ноч","ночь");A("рож","рожь");A("тиш","тишь");
394            A("доч","дочь");A("печ","печь");A("реч","речь");A("вещ","вещь");
395
396            // НН/Н В ПРИЛАГАТЕЛЬНЫХ
397            A("деревяный","деревянный");A("стекляный","стеклянный");A("оловяный","оловянный");
398            A("искуственный","искусственный");
399            A("серебряный","серебряный");// correct — одна Н
400            A("ветреный","ветреный");// correct — одна Н (исключение)
401            A("соломеный","соломенный");A("клюквеный","клюквенный");
402            A("торжественый","торжественный");A("государственый","государственный");
403
404            // ПРИСТАВКИ РАЗ-/РАС-
405            A("расбить","разбить");A("расбудить","разбудить");A("расговор","разговор");
406            A("исправить","исправить");// correct
407            A("изправить","исправить");
408            A("бесполезный","бесполезный");// correct
409            A("безполезный","бесполезный");
410
411            // СЛИТНОЕ/РАЗДЕЛЬНОЕ НАРЕЧИЯ
412            A("впринципе","в принципе");
413            A("вкурсе","в курсе");A("втечение","в течение");A("втечении","в течении");
414            A("впоследствии","впоследствии");// correct, слитно
415            A("насчёт","насчёт");// correct, слитно
416            A("поэтому","поэтому");// correct, слитно
417            A("потомучто","потому что");A("потомушто","потому что");
418        }
419
420        // === Auto-generation from word patterns ===
421
422        private static int GenPhoneticErrors(string w)
423        {
424            int c = 0;
425            char[] ch = w.ToCharArray();
426            for (int i = 1; i < ch.Length - 1; i++) // skip first/last char
427            {
428                foreach (var pair in vowelPairs)
429                {
430                    if (ch[i] == pair[0])
431                    {
432                        ch[i] = pair[1];
433                        string err = new string(ch);
434                        if (err != w && !corrections.ContainsKey(err)) { corrections[err] = w; c++; }
435                        ch[i] = pair[0];
436                    }
437                }
438            }
439            return c;
440        }
441
442        private static int GenMissingSoftSign(string w)
443        {
444            int c = 0;
445            if (w.EndsWith("ь") && w.Length >= 4)
446            {
447                string no = w.Substring(0, w.Length - 1);
448                if (!corrections.ContainsKey(no)) { corrections[no] = w; c++; }
449            }
450            return c;
451        }
452
453        private static int GenDoubleConsonant(string w)
454        {
455            int c = 0;
456            for (int i = 0; i < w.Length - 1; i++)
457            {
458                if (w[i] == w[i + 1] && !IsVowel(w[i]))
459                {
460                    string err = w.Remove(i, 1);
461                    if (err.Length >= 3 && !corrections.ContainsKey(err)) { corrections[err] = w; c++; }
462                }
463            }
464            return c;
465        }
466
467        private static int GenEndVoicing(string w)
468        {
469            int c = 0;
470            if (w.Length < 3) return 0;
471            char last = w[w.Length - 1];
472            char voiceless;
473            if (voicedToVoiceless.TryGetValue(last, out voiceless))
474            {
475                string err = w.Substring(0, w.Length - 1) + voiceless;
476                if (!corrections.ContainsKey(err)) { corrections[err] = w; c++; }
477            }
478            return c;
479        }
480
481        private static int GenTsyaErrors(string w)
482        {
483            int c = 0;
484            if (w.EndsWith("тся") && w.Length >= 5)
485            {
486                string stem = w.Substring(0, w.Length - 3);
487                A2(stem + "ца", w, ref c);
488                A2(stem + "цца", w, ref c);
489            }
490            if (w.EndsWith("ться") && w.Length >= 6)
491            {
492                string stem = w.Substring(0, w.Length - 4);
493                A2(stem + "ца", w, ref c);
494                A2(stem + "цца", w, ref c);
495            }
496            return c;
497        }
498
499        private static int GenMissingSh(string w)
500        {
501            int c = 0;
502            // "пишешь"→"пишеш", "говоришь"→"говориш"
503            if (w.EndsWith("шь") && w.Length >= 4)
504            {
505                string err = w.Substring(0, w.Length - 1); // drop ь
506                if (!corrections.ContainsKey(err)) { corrections[err] = w; c++; }
507            }
508            // "идёшь"→"идёш"
509            if (w.EndsWith("шь") && w.Length >= 3)
510            {
511                string err = w.Substring(0, w.Length - 1);
512                if (!corrections.ContainsKey(err)) { corrections[err] = w; c++; }
513            }
514            return c;
515        }
516
517        private static int GenDoubleVowelDrop(string w)
518        {
519            // Already mostly covered by GenDoubleConsonant
520            return 0;
521        }
522
523        // === NEW: Unpronounced consonants ===
524        // "солнце"→"сонце", "сердце"→"серце", "лестница"→"лесница"
525        private static int GenUnpronouncedConsonant(string w)
526        {
527            int c = 0;
528            // Common patterns: стн→сн, стл→сл, здн→зн, рдц→рц, лнц→нц
529            string[][] patterns = {
530                new[]{"стн","сн"}, new[]{"стл","сл"}, new[]{"здн","зн"},
531                new[]{"рдц","рц"}, new[]{"лнц","нц"}, new[]{"вств","ств"},
532                new[]{"ндш","нш"}, new[]{"нтск","нск"}
533            };
534            foreach (var pat in patterns)
535            {
536                int idx = w.IndexOf(pat[0]);
537                if (idx >= 0)
538                {
539                    string err = w.Substring(0, idx) + pat[1] + w.Substring(idx + pat[0].Length);
540                    if (!corrections.ContainsKey(err)) { corrections[err] = w; c++; }
541                }
542            }
543            return c;
544        }
545
546        // === NEW: Ё after шипящие ===
547        // "шопот"→"шёпот", "жолудь"→"жёлудь", "чорный"→"чёрный"
548        private static int GenYoAfterSh(string w)
549        {
550            int c = 0;
551            if (!w.Contains("ё")) return 0;
552            // Replace ё with о — that's how people misspell
553            string err = w.Replace('ё', 'о');
554            if (err != w && !corrections.ContainsKey(err)) { corrections[err] = w; c++; }
555            // Also е→ё is tricky but common: "еще"→"ещё" — handled by dictionary
556            return c;
557        }
558
559        // === NEW: Ъ/Ь разделительные ===
560        // "обем"→"объём", "семка"→"съёмка", "подезд"→"подъезд"
561        private static int GenHardSoftSign(string w)
562        {
563            int c = 0;
564            // After prefixes ending in consonant + ъ before е,ё,ю,я
565            int idx = w.IndexOf('ъ');
566            if (idx > 0 && idx < w.Length - 1)
567            {
568                string err = w.Remove(idx, 1); // drop ъ
569                if (!corrections.ContainsKey(err)) { corrections[err] = w; c++; }
570            }
571            // ь in middle of words
572            idx = w.IndexOf('ь');
573            if (idx > 0 && idx < w.Length - 1 && "еёюяи".IndexOf(w[idx + 1]) >= 0)
574            {
575                string err = w.Remove(idx, 1);
576                if (!corrections.ContainsKey(err)) { corrections[err] = w; c++; }
577            }
578            return c;
579        }
580
581        // === NEW: Ь after шипящие (3 склонение) ===
582        // "мышь","ночь","рожь","тишь" — if word ends in ш,щ,ч,ж + ь
583        // "мыш"→"мышь", "ноч"→"ночь"
584        private static int GenShSoftSign(string w)
585        {
586            int c = 0;
587            if (w.Length >= 3 && w.EndsWith("ь"))
588            {
589                char prev = w[w.Length - 2];
590                if ("шщчж".IndexOf(prev) >= 0)
591                {
592                    string err = w.Substring(0, w.Length - 1); // drop ь
593                    if (!corrections.ContainsKey(err)) { corrections[err] = w; c++; }
594                }
595            }
596            return c;
597        }
598
599        // === NEW: Prefix з/с before voiced/voiceless ===
600        // "расбить"→"разбить", "избросить"→"исбросить" (wrong)
601        // Rule: з before voiced, с before voiceless
602        private static int GenPrefixZS(string w)
603        {
604            // Word `w` is CORRECT (from dictionary). Generate common ERRORS that map to it.
605            // Rule: з before voiced, с before voiceless.
606            // "разбить" is correct (з before б=voiced) → error "расбить" maps to "разбить"
607            // "расписать" is correct (с before п=voiceless) → error "разписать" maps to "расписать"
608            int c = 0;
609            string[] prefixPairsZ = { "раз","без","из","воз","вз","низ","через" };
610            string[] prefixPairsS = { "рас","бес","ис","вос","вс","нис","черес" };
611
612            for (int p = 0; p < prefixPairsZ.Length; p++)
613            {
614                string zPfx = prefixPairsZ[p];
615                string sPfx = prefixPairsS[p];
616
617                // Word has з-prefix (correct before voiced) → error would be с-prefix
618                if (w.StartsWith(zPfx) && w.Length > zPfx.Length + 1)
619                {
620                    string error = sPfx + w.Substring(zPfx.Length); // "расбить" for "разбить"
621                    if (!corrections.ContainsKey(error)) { corrections[error] = w; c++; }
622                }
623                // Word has с-prefix (correct before voiceless) → error would be з-prefix
624                if (w.StartsWith(sPfx) && w.Length > sPfx.Length + 1)
625                {
626                    string error = zPfx + w.Substring(sPfx.Length); // "разписать" for "расписать"
627                    if (!corrections.ContainsKey(error)) { corrections[error] = w; c++; }
628                }
629            }
630            return c;
631        }
632
633        // ===== Helpers =====
634        private static void A(string err, string correct)
635        {
636            if (!corrections.ContainsKey(err)) corrections[err] = correct;
637        }
638        private static void A2(string err, string correct, ref int count)
639        {
640            if (!corrections.ContainsKey(err)) { corrections[err] = correct; count++; }
641        }
642        private static bool IsVowel(char c) { return "аеёиоуыэюя".IndexOf(c) >= 0; }
643        private static bool EndsWith(string w, params string[] suffixes)
644        {
645            foreach (var s in suffixes) if (w.Length >= s.Length && w.EndsWith(s)) return true;
646            return false;
647        }
648        private static string ReplaceWholeWord(string text, string find, string replace)
649        {
650            int idx = 0;
651            string lower = text.ToLower();
652            while ((idx = lower.IndexOf(find, idx)) >= 0)
653            {
654                bool l = idx == 0 || !char.IsLetter(text[idx - 1]);
655                bool r = idx + find.Length >= text.Length || !char.IsLetter(text[idx + find.Length]);
656                if (l && r)
657                {
658                    text = text.Substring(0, idx) + replace + text.Substring(idx + find.Length);
659                    lower = text.ToLower();
660                    idx += replace.Length;
661                }
662                else idx++;
663            }
664            return text;
665        }
666    }
667}