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}