Skip to main content

rust_switcher_core/text/
mapping.rs

1// File: src/domain/text/mapping.rs
2
3#[derive(Copy, Clone, Debug, Eq, PartialEq)]
4pub enum ConversionDirection {
5    RuToEn,
6    EnToRu,
7}
8
9fn is_latin_letter(ch: char) -> bool {
10    ch.is_ascii_alphabetic()
11}
12
13fn is_cyrillic_letter(ch: char) -> bool {
14    matches!(ch, 'А'..='Я' | 'а'..='я' | 'Ё' | 'ё')
15}
16
17fn map_ru_to_en(ch: char) -> char {
18    match ch {
19        // punctuation rules (for . , ? keys)
20        ',' => '?',
21        '.' => '/',
22        '?' => '&',
23
24        // number row shift symbols (RU -> EN, same physical keys)
25        '"' => '@', // RU Shift+2 -> EN Shift+2
26        '№' => '#', // RU Shift+3 -> EN Shift+3
27        ';' => '$', // RU Shift+4 -> EN Shift+4
28        ':' => '^', // RU Shift+6 -> EN Shift+6
29        // RU Shift+7 is '?' -> '&' already covered above
30        'й' => 'q',
31        'ц' => 'w',
32        'у' => 'e',
33        'к' => 'r',
34        'е' => 't',
35        'н' => 'y',
36        'г' => 'u',
37        'ш' => 'i',
38        'щ' => 'o',
39        'з' => 'p',
40        'х' => '[',
41        'ъ' => ']',
42        'ф' => 'a',
43        'ы' => 's',
44        'в' => 'd',
45        'а' => 'f',
46        'п' => 'g',
47        'р' => 'h',
48        'о' => 'j',
49        'л' => 'k',
50        'д' => 'l',
51        'ж' => ';',
52        'э' => '\'',
53        'я' => 'z',
54        'ч' => 'x',
55        'с' => 'c',
56        'м' => 'v',
57        'и' => 'b',
58        'т' => 'n',
59        'ь' => 'm',
60        'б' => ',',
61        'ю' => '.',
62        'ё' => '`',
63
64        'Й' => 'Q',
65        'Ц' => 'W',
66        'У' => 'E',
67        'К' => 'R',
68        'Е' => 'T',
69        'Н' => 'Y',
70        'Г' => 'U',
71        'Ш' => 'I',
72        'Щ' => 'O',
73        'З' => 'P',
74        'Х' => '{',
75        'Ъ' => '}',
76        'Ф' => 'A',
77        'Ы' => 'S',
78        'В' => 'D',
79        'А' => 'F',
80        'П' => 'G',
81        'Р' => 'H',
82        'О' => 'J',
83        'Л' => 'K',
84        'Д' => 'L',
85        'Ж' => ':',
86        'Э' => '"',
87        'Я' => 'Z',
88        'Ч' => 'X',
89        'С' => 'C',
90        'М' => 'V',
91        'И' => 'B',
92        'Т' => 'N',
93        'Ь' => 'M',
94        'Б' => '<',
95        'Ю' => '>',
96        'Ё' => '~',
97        _ => ch,
98    }
99}
100
101fn map_en_to_ru(ch: char) -> char {
102    match ch {
103        // letters / punctuation keys (EN -> RU)
104        'q' => 'й',
105        'w' => 'ц',
106        'e' => 'у',
107        'r' => 'к',
108        't' => 'е',
109        'y' => 'н',
110        'u' => 'г',
111        'i' => 'ш',
112        'o' => 'щ',
113        'p' => 'з',
114        '[' => 'х',
115        ']' => 'ъ',
116        'a' => 'ф',
117        's' => 'ы',
118        'd' => 'в',
119        'f' => 'а',
120        'g' => 'п',
121        'h' => 'р',
122        'j' => 'о',
123        'k' => 'л',
124        'l' => 'д',
125        ';' => 'ж',
126        '\'' => 'э',
127        'z' => 'я',
128        'x' => 'ч',
129        'c' => 'с',
130        'v' => 'м',
131        'b' => 'и',
132        'n' => 'т',
133        'm' => 'ь',
134        ',' => 'б',
135        '.' => 'ю',
136        '`' => 'ё',
137
138        // punctuation rules (for . , ? keys)
139        '?' => ',',
140        '/' => '.',
141        '&' => '?',
142
143        // number row shift symbols (EN -> RU, same physical keys)
144        '@' => '"', // EN Shift+2 -> RU Shift+2
145        '#' => '№', // EN Shift+3 -> RU Shift+3
146        '$' => ';', // EN Shift+4 -> RU Shift+4
147        '^' => ':', // EN Shift+6 -> RU Shift+6
148        // EN Shift+7 is '&' -> '?' already covered above
149
150        // shifted letters / punctuation keys (EN -> RU)
151        'Q' => 'Й',
152        'W' => 'Ц',
153        'E' => 'У',
154        'R' => 'К',
155        'T' => 'Е',
156        'Y' => 'Н',
157        'U' => 'Г',
158        'I' => 'Ш',
159        'O' => 'Щ',
160        'P' => 'З',
161        '{' => 'Х',
162        '}' => 'Ъ',
163        'A' => 'Ф',
164        'S' => 'Ы',
165        'D' => 'В',
166        'F' => 'А',
167        'G' => 'П',
168        'H' => 'Р',
169        'J' => 'О',
170        'K' => 'Л',
171        'L' => 'Д',
172        ':' => 'Ж',
173        '"' => 'Э',
174        'Z' => 'Я',
175        'X' => 'Ч',
176        'C' => 'С',
177        'V' => 'М',
178        'B' => 'И',
179        'N' => 'Т',
180        'M' => 'Ь',
181        '<' => 'Б',
182        '>' => 'Ю',
183        '~' => 'Ё',
184        _ => ch,
185    }
186}
187
188fn letter_counts(text: &str) -> (usize, usize) {
189    let mut cyr = 0usize;
190    let mut lat = 0usize;
191    for ch in text.chars() {
192        if is_cyrillic_letter(ch) {
193            cyr += 1;
194        } else if is_latin_letter(ch) {
195            lat += 1;
196        }
197    }
198    (cyr, lat)
199}
200
201/// Returns a conversion direction based on letter balance.
202///
203/// If the counts are tied (including zero letters), returns `None`.
204pub fn conversion_direction_for_text(text: &str) -> Option<ConversionDirection> {
205    let (cyr, lat) = letter_counts(text);
206    match cyr.cmp(&lat) {
207        std::cmp::Ordering::Greater => Some(ConversionDirection::RuToEn),
208        std::cmp::Ordering::Less => Some(ConversionDirection::EnToRu),
209        std::cmp::Ordering::Equal => None,
210    }
211}
212
213/// Converts text between English QWERTY and Russian ЙЦУКЕН keyboard layouts in the given direction.
214pub fn convert_ru_en_with_direction(text: &str, direction: ConversionDirection) -> String {
215    let mut out = String::with_capacity(text.len());
216    match direction {
217        ConversionDirection::RuToEn => {
218            for ch in text.chars() {
219                out.push(map_ru_to_en(ch));
220            }
221        }
222        ConversionDirection::EnToRu => {
223            for ch in text.chars() {
224                out.push(map_en_to_ru(ch));
225            }
226        }
227    }
228    out
229}
230
231/// Convenience wrapper: auto-detect direction (fallback to RuToEn on ties).
232/// This is intentionally a normal public API so downstream test crates can use it.
233pub fn convert_ru_en_bidirectional(text: &str) -> String {
234    let direction = conversion_direction_for_text(text).unwrap_or(ConversionDirection::RuToEn);
235    convert_ru_en_with_direction(text, direction)
236}