Skip to main content

embedded_i18n/
range.rs

1use alloc::format;
2use alloc::string::String;
3use alloc::vec::Vec;
4use core::ops::Range;
5
6pub const BASIC_LATIN: Range<u32> = 0x20..0x7F;
7pub const LATIN_1_SUPPLEMENT: Range<u32> = 0xA0..0xFF;
8pub const LATIN_EXTENDED_A: Range<u32> = 0x100..0x17F;
9pub const LATIN_EXTENDED_B: Range<u32> = 0x180..0x24F;
10pub const LATIN_EXTENDED_ADDITIONAL: Range<u32> = 0x1E00..0x1EFF;
11pub const CYRILLIC: Range<u32> = 0x400..0x4FF;
12pub const CYRILLIC_SUPPLEMENT: Range<u32> = 0x500..0x52F;
13pub const GREEK: Range<u32> = 0x370..0x3FF;
14pub const GREEK_EXTENDED: Range<u32> = 0x1F00..0x1FFF;
15pub const ARABIC: Range<u32> = 0x600..0x6FF;
16pub const ARABIC_SUPPLEMENT: Range<u32> = 0x750..0x77F;
17pub const HEBREW: Range<u32> = 0x590..0x5FF;
18pub const CJK_UNIFIED_IDEOGRAPHS: Range<u32> = 0x4E00..0x9FFF;
19pub const HIRAGANA: Range<u32> = 0x3040..0x309F;
20pub const KATAKANA: Range<u32> = 0x30A0..0x30FF;
21pub const HANGUL_SYLLABLES: Range<u32> = 0xAC00..0xD7AF;
22pub const DEVANAGARI: Range<u32> = 0x900..0x97F;
23pub const THAI: Range<u32> = 0xE00..0xE7F;
24pub const VIETNAMESE_EXTENSIONS: Range<u32> = 0x1EA0..0x1EFF;
25
26pub fn get_locale_ranges(locale: &str) -> Option<&'static [Range<u32>]> {
27    let ranges: &[Range<u32>] = match locale {
28        "en" => &[BASIC_LATIN],
29
30        "fr" => &[
31            BASIC_LATIN,
32            LATIN_1_SUPPLEMENT,
33            LATIN_EXTENDED_A,
34            LATIN_EXTENDED_B,
35        ],
36
37        "de" => &[BASIC_LATIN, LATIN_1_SUPPLEMENT, LATIN_EXTENDED_A],
38
39        "es" => &[BASIC_LATIN, LATIN_1_SUPPLEMENT],
40
41        "it" => &[BASIC_LATIN, LATIN_1_SUPPLEMENT],
42
43        "pt" => &[BASIC_LATIN, LATIN_1_SUPPLEMENT, LATIN_EXTENDED_A],
44
45        "nl" => &[BASIC_LATIN, LATIN_1_SUPPLEMENT],
46
47        "sv" | "no" | "da" => &[BASIC_LATIN, LATIN_1_SUPPLEMENT],
48
49        "pl" => &[BASIC_LATIN, LATIN_1_SUPPLEMENT, LATIN_EXTENDED_A],
50
51        "cs" | "sk" => &[BASIC_LATIN, LATIN_1_SUPPLEMENT, LATIN_EXTENDED_A],
52
53        "hu" => &[BASIC_LATIN, LATIN_1_SUPPLEMENT, LATIN_EXTENDED_A],
54
55        "ro" => &[BASIC_LATIN, LATIN_1_SUPPLEMENT, LATIN_EXTENDED_A],
56
57        "tr" => &[BASIC_LATIN, LATIN_1_SUPPLEMENT, LATIN_EXTENDED_A],
58
59        "ru" | "uk" | "be" => &[BASIC_LATIN, CYRILLIC, CYRILLIC_SUPPLEMENT],
60
61        "el" => &[BASIC_LATIN, GREEK, GREEK_EXTENDED],
62
63        "ar" => &[BASIC_LATIN, ARABIC, ARABIC_SUPPLEMENT],
64
65        "he" => &[BASIC_LATIN, HEBREW],
66
67        "ja" => &[BASIC_LATIN, HIRAGANA, KATAKANA, CJK_UNIFIED_IDEOGRAPHS],
68
69        "zh" | "zh-CN" | "zh-TW" => &[BASIC_LATIN, CJK_UNIFIED_IDEOGRAPHS],
70
71        "ko" => &[BASIC_LATIN, HANGUL_SYLLABLES, CJK_UNIFIED_IDEOGRAPHS],
72
73        "hi" => &[BASIC_LATIN, DEVANAGARI],
74
75        "th" => &[BASIC_LATIN, THAI],
76
77        "vi" => &[
78            BASIC_LATIN,
79            LATIN_1_SUPPLEMENT,
80            LATIN_EXTENDED_A,
81            LATIN_EXTENDED_ADDITIONAL,
82            VIETNAMESE_EXTENSIONS,
83        ],
84        _ => return None,
85    };
86
87    Some(ranges)
88}
89
90pub fn merge_contiguous_ranges(ranges: Vec<Range<u32>>) -> Vec<Range<u32>> {
91    if ranges.is_empty() {
92        return ranges;
93    }
94
95    let mut sorted_ranges = ranges;
96    sorted_ranges.sort_by_key(|r| r.start);
97
98    let mut merged_ranges = Vec::new();
99    let mut current_range = sorted_ranges[0].clone();
100
101    for range in sorted_ranges.into_iter().skip(1) {
102        if range.start <= current_range.end {
103            current_range.end = current_range.end.max(range.end);
104        } else {
105            merged_ranges.push(current_range);
106            current_range = range;
107        }
108    }
109
110    merged_ranges.push(current_range);
111
112    merged_ranges
113}
114
115pub fn format_range(range: &Range<u32>) -> String {
116    if range.start + 1 == range.end {
117        format!("{}", range.start)
118    } else {
119        format!("{}-{}", range.start, range.end - 1)
120    }
121}
122
123pub fn format_ranges<'a>(ranges: impl IntoIterator<Item = &'a Range<u32>>) -> String {
124    ranges
125        .into_iter()
126        .map(format_range)
127        .collect::<Vec<String>>()
128        .join(",")
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134
135    use alloc::vec;
136
137    #[test]
138    fn test_get_locale_ranges_english() {
139        let ranges = get_locale_ranges("en").unwrap();
140        assert_eq!(ranges.len(), 1);
141        assert_eq!(ranges[0], BASIC_LATIN);
142    }
143
144    #[test]
145    fn test_get_locale_ranges_french() {
146        let ranges = get_locale_ranges("fr").unwrap();
147        assert_eq!(ranges.len(), 4);
148        assert!(ranges.contains(&BASIC_LATIN));
149        assert!(ranges.contains(&LATIN_1_SUPPLEMENT));
150    }
151
152    #[test]
153    fn test_get_locale_ranges_russian() {
154        let ranges = get_locale_ranges("ru").unwrap();
155        assert_eq!(ranges.len(), 3);
156        assert!(ranges.contains(&CYRILLIC));
157    }
158
159    #[test]
160    fn test_get_locale_ranges_japanese() {
161        let ranges = get_locale_ranges("ja").unwrap();
162        assert_eq!(ranges.len(), 4);
163        assert!(ranges.contains(&HIRAGANA));
164        assert!(ranges.contains(&KATAKANA));
165        assert!(ranges.contains(&CJK_UNIFIED_IDEOGRAPHS));
166    }
167
168    #[test]
169    fn test_get_locale_ranges_invalid() {
170        assert!(get_locale_ranges("invalid").is_none());
171        assert!(get_locale_ranges("xx").is_none());
172    }
173
174    #[test]
175    fn test_merge_contiguous_ranges_empty() {
176        let ranges = vec![];
177        let merged = merge_contiguous_ranges(ranges);
178        assert_eq!(merged.len(), 0);
179    }
180
181    #[test]
182    fn test_merge_contiguous_ranges_single() {
183        let ranges = vec![Range {
184            start: 0x20,
185            end: 0x7F,
186        }];
187        let merged = merge_contiguous_ranges(ranges);
188        assert_eq!(merged.len(), 1);
189        assert_eq!(merged[0], 0x20..0x7F);
190    }
191
192    #[test]
193    fn test_merge_contiguous_ranges_adjacent() {
194        let ranges = vec![0x20..0x7F, 0x7F..0xFF];
195        let merged = merge_contiguous_ranges(ranges);
196        assert_eq!(merged.len(), 1);
197        assert_eq!(merged[0], 0x20..0xFF);
198    }
199
200    #[test]
201    fn test_merge_contiguous_ranges_overlapping() {
202        let ranges = vec![0x20..0x80, 0x50..0xFF];
203        let merged = merge_contiguous_ranges(ranges);
204        assert_eq!(merged.len(), 1);
205        assert_eq!(merged[0], 0x20..0xFF);
206    }
207
208    #[test]
209    fn test_merge_contiguous_ranges_non_overlapping() {
210        let ranges = vec![0x20..0x7F, 0x100..0x17F];
211        let merged = merge_contiguous_ranges(ranges);
212        assert_eq!(merged.len(), 2);
213        assert_eq!(merged[0], 0x20..0x7F);
214        assert_eq!(merged[1], 0x100..0x17F);
215    }
216
217    #[test]
218    fn test_merge_contiguous_ranges_unsorted() {
219        let ranges = vec![0x100..0x17F, 0x20..0x7F, 0x7F..0xFF];
220        let merged = merge_contiguous_ranges(ranges);
221        assert_eq!(merged.len(), 2);
222        assert_eq!(merged[0], 0x20..0xFF);
223        assert_eq!(merged[1], 0x100..0x17F);
224    }
225
226    #[test]
227    fn test_merge_contiguous_ranges_multiple_groups() {
228        let ranges = vec![0x20..0x7F, 0x7F..0xFF, 0x200..0x2FF, 0x2FF..0x3FF];
229        let merged = merge_contiguous_ranges(ranges);
230        assert_eq!(merged.len(), 2);
231        assert_eq!(merged[0], 0x20..0xFF);
232        assert_eq!(merged[1], 0x200..0x3FF);
233    }
234
235    #[test]
236    fn test_unicode_range_boundaries() {
237        assert_eq!(BASIC_LATIN.start, 0x20);
238        assert_eq!(BASIC_LATIN.end, 0x7F);
239        assert_eq!(CJK_UNIFIED_IDEOGRAPHS.start, 0x4E00);
240        assert_eq!(CJK_UNIFIED_IDEOGRAPHS.end, 0x9FFF);
241    }
242
243    #[test]
244    fn test_get_locale_ranges_aliases() {
245        assert!(get_locale_ranges("zh").is_some());
246        assert!(get_locale_ranges("zh-CN").is_some());
247        assert!(get_locale_ranges("zh-TW").is_some());
248
249        assert!(get_locale_ranges("sv").is_some());
250        assert!(get_locale_ranges("no").is_some());
251        assert!(get_locale_ranges("da").is_some());
252    }
253
254    #[test]
255    fn test_all_locales_include_basic_latin() {
256        let locales = [
257            "en", "fr", "de", "es", "it", "pt", "nl", "sv", "no", "da", "pl", "cs", "sk", "hu",
258            "ro", "tr", "ru", "uk", "be", "el", "ar", "he", "ja", "zh", "ko", "hi", "th", "vi",
259        ];
260        for locale in locales {
261            let ranges = get_locale_ranges(locale)
262                .unwrap_or_else(|| panic!("{} should have ranges", locale));
263            assert!(
264                ranges.contains(&BASIC_LATIN),
265                "{} should include BASIC_LATIN",
266                locale
267            );
268        }
269    }
270
271    #[test]
272    fn test_get_locale_ranges_german() {
273        let ranges = get_locale_ranges("de").unwrap();
274        assert_eq!(ranges.len(), 3);
275        assert!(ranges.contains(&LATIN_1_SUPPLEMENT));
276        assert!(ranges.contains(&LATIN_EXTENDED_A));
277    }
278
279    #[test]
280    fn test_get_locale_ranges_arabic() {
281        let ranges = get_locale_ranges("ar").unwrap();
282        assert!(ranges.contains(&ARABIC));
283        assert!(ranges.contains(&ARABIC_SUPPLEMENT));
284    }
285
286    #[test]
287    fn test_get_locale_ranges_hebrew() {
288        let ranges = get_locale_ranges("he").unwrap();
289        assert!(ranges.contains(&HEBREW));
290    }
291
292    #[test]
293    fn test_get_locale_ranges_korean() {
294        let ranges = get_locale_ranges("ko").unwrap();
295        assert!(ranges.contains(&HANGUL_SYLLABLES));
296        assert!(ranges.contains(&CJK_UNIFIED_IDEOGRAPHS));
297    }
298
299    #[test]
300    fn test_get_locale_ranges_vietnamese() {
301        let ranges = get_locale_ranges("vi").unwrap();
302        assert!(ranges.contains(&VIETNAMESE_EXTENSIONS));
303        assert!(ranges.contains(&LATIN_EXTENDED_ADDITIONAL));
304    }
305
306    #[test]
307    fn test_format_range_single() {
308        let range = 0x41..0x42;
309        assert_eq!(format_range(&range), "65");
310    }
311
312    #[test]
313    fn test_format_range_normal() {
314        let range = 0x20..0x7F;
315        assert_eq!(format_range(&range), "32-126");
316    }
317
318    #[test]
319    fn test_format_range_large() {
320        let range = 0x4E00..0x9FFF;
321        assert_eq!(format_range(&range), "19968-40958");
322    }
323
324    #[test]
325    fn test_format_ranges_empty() {
326        let ranges: Vec<&Range<u32>> = vec![];
327        assert_eq!(format_ranges(ranges), "");
328    }
329
330    #[test]
331    fn test_format_ranges_single() {
332        let range = 0x20..0x7F;
333        assert_eq!(format_ranges(core::iter::once(&range)), "32-126");
334    }
335
336    #[test]
337    fn test_format_ranges_multiple() {
338        let ranges = vec![&BASIC_LATIN, &LATIN_1_SUPPLEMENT];
339        let result = format_ranges(ranges);
340        assert_eq!(result, "32-126,160-254");
341    }
342
343    #[test]
344    fn test_merge_contiguous_ranges_exactly_adjacent() {
345        let ranges = vec![0x20..0x7F, 0x7F..0xFF];
346        let merged = merge_contiguous_ranges(ranges);
347        assert_eq!(merged.len(), 1);
348        assert_eq!(merged[0], 0x20..0xFF);
349    }
350
351    #[test]
352    fn test_merge_contiguous_ranges_not_touching() {
353        let ranges = vec![0x20..0x7F, 0x100..0x17F, 0x200..0x2FF];
354        let merged = merge_contiguous_ranges(ranges);
355        assert_eq!(merged.len(), 3);
356    }
357
358    #[test]
359    fn test_merge_contiguous_ranges_partial_overlap() {
360        let ranges = vec![0x20..0x80, 0x60..0xA0, 0x90..0xFF];
361        let merged = merge_contiguous_ranges(ranges);
362        assert_eq!(merged.len(), 1);
363        assert_eq!(merged[0], 0x20..0xFF);
364    }
365
366    #[test]
367    fn test_get_locale_ranges_case_sensitive() {
368        assert!(get_locale_ranges("EN").is_none());
369        assert!(get_locale_ranges("FR").is_none());
370    }
371
372    #[test]
373    fn test_get_locale_ranges_polish() {
374        let ranges = get_locale_ranges("pl").unwrap();
375        assert!(ranges.contains(&LATIN_EXTENDED_A));
376    }
377
378    #[test]
379    fn test_get_locale_ranges_turkish() {
380        let ranges = get_locale_ranges("tr").unwrap();
381        assert!(ranges.contains(&LATIN_1_SUPPLEMENT));
382    }
383}