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}