Skip to main content

rust_switcher_core/text/
mapping.rs

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