1#![cfg_attr(test, feature(test))]
49#![deny(missing_docs, unsafe_code)]
50
51use core::{char, fmt};
52use generated::{
53 MAX_NAME_LENGTH, PHRASEBOOK_OFFSET_SHIFT, PHRASEBOOK_OFFSETS1, PHRASEBOOK_OFFSETS2,
54};
55
56mod generated {
57 include!(concat!(env!("OUT_DIR"), "/generated.rs"));
58}
59mod generated_phf {
60 include!(concat!(env!("OUT_DIR"), "/generated_phf.rs"));
61}
62mod jamo;
63
64static ALIASES: phf::Map<&'static [u8], char> =
67 include!(concat!(env!("OUT_DIR"), "/generated_alias.rs"));
68
69mod iter_str;
70
71static HANGUL_SYLLABLE_PREFIX: &str = "HANGUL SYLLABLE ";
72static CJK_UNIFIED_IDEOGRAPH_PREFIX: &str = "CJK UNIFIED IDEOGRAPH-";
73
74fn is_cjk_unified_ideograph(ch: char) -> bool {
75 generated::CJK_IDEOGRAPH_RANGES
76 .iter()
77 .any(|&(lo, hi)| lo <= ch && ch <= hi)
78}
79
80#[derive(Clone)]
86pub struct Name {
87 data: Name_,
88}
89#[allow(clippy::upper_case_acronyms)]
90#[derive(Clone)]
91enum Name_ {
92 Plain(iter_str::IterStr),
93 CJK(CJK),
94 Hangul(Hangul),
95}
96
97#[allow(clippy::upper_case_acronyms)]
98#[derive(Copy)]
99struct CJK {
100 emit_prefix: bool,
101 idx: u8,
102 data: [u8; 6],
104}
105#[derive(Copy)]
106struct Hangul {
107 emit_prefix: bool,
108 idx: u8,
109 data: [u8; 3],
112}
113impl Clone for CJK {
114 fn clone(&self) -> CJK {
115 *self
116 }
117}
118impl Clone for Hangul {
119 fn clone(&self) -> Hangul {
120 *self
121 }
122}
123
124#[allow(clippy::len_without_is_empty)]
125impl Name {
126 pub fn len(&self) -> usize {
131 let counted = self.clone();
132 counted.fold(0, |a, s| a + s.len())
133 }
134}
135
136impl Iterator for Name {
137 type Item = &'static str;
138 fn next(&mut self) -> Option<&'static str> {
139 match self.data {
140 Name_::Plain(ref mut s) => s.next(),
141 Name_::CJK(ref mut state) => {
142 if state.emit_prefix {
144 state.emit_prefix = false;
145 return Some(CJK_UNIFIED_IDEOGRAPH_PREFIX);
146 }
147 state
151 .data
152 .get(state.idx as usize)
153 .map(|digit| *digit as usize)
155 .map(|d| {
156 state.idx += 1;
157 static DIGITS: &str = "0123456789ABCDEF";
158 &DIGITS[d..d + 1]
159 })
160 }
161 Name_::Hangul(ref mut state) => {
162 if state.emit_prefix {
163 state.emit_prefix = false;
164 return Some(HANGUL_SYLLABLE_PREFIX);
165 }
166
167 let idx = state.idx as usize;
168 state.data.get(idx).map(|x| *x as usize).map(|x| {
169 state.idx += 1;
171 [jamo::CHOSEONG, jamo::JUNGSEONG, jamo::JONGSEONG][idx][x]
172 })
173 }
174 }
175 }
176
177 fn size_hint(&self) -> (usize, Option<usize>) {
178 let counted = self.clone();
180 let n = counted.count();
181 (n, Some(n))
182 }
183}
184
185impl fmt::Debug for Name {
186 fn fmt(&self, fmtr: &mut fmt::Formatter) -> fmt::Result {
187 fmt::Display::fmt(self, fmtr)
188 }
189}
190impl fmt::Display for Name {
191 fn fmt(&self, fmtr: &mut fmt::Formatter) -> fmt::Result {
192 self.clone().try_for_each(|s| fmtr.write_str(s))
193 }
194}
195
196pub fn name(c: char) -> Option<Name> {
219 let cc = c as usize;
220 let offset =
221 (PHRASEBOOK_OFFSETS1[cc >> PHRASEBOOK_OFFSET_SHIFT] as usize) << PHRASEBOOK_OFFSET_SHIFT;
222
223 let mask = (1 << PHRASEBOOK_OFFSET_SHIFT) - 1;
224 let offset = PHRASEBOOK_OFFSETS2[offset + (cc & mask)];
225 if offset == 0 {
226 if is_cjk_unified_ideograph(c) {
227 let mut data = [b'0'; 6];
229 let mut number = c as u32;
230 let mut data_start = 6;
231 for place in data.iter_mut().rev() {
232 if number == 0 {
235 break;
236 }
237 *place = (number % 16) as u8;
238 number /= 16;
239 data_start -= 1;
240 }
241 Some(Name {
242 data: Name_::CJK(CJK {
243 emit_prefix: true,
244 idx: data_start,
245 data,
246 }),
247 })
248 } else {
249 jamo::syllable_decomposition(c).map(|(ch, ju, jo)| Name {
251 data: Name_::Hangul(Hangul {
252 emit_prefix: true,
253 idx: 0,
254 data: [ch, ju, jo],
255 }),
256 })
257 }
258 } else {
259 Some(Name {
260 data: Name_::Plain(iter_str::IterStr::new(offset)),
261 })
262 }
263}
264
265fn fnv_hash<I: Iterator<Item = u8>>(x: I) -> u64 {
266 let mut g = 0xcbf29ce484222325 ^ generated_phf::NAME2CODE_N;
267 for b in x {
268 g ^= b as u64;
269 g = g.wrapping_mul(0x100000001b3);
270 }
271 g
272}
273fn displace(f1: u32, f2: u32, d1: u32, d2: u32) -> u32 {
274 d2.wrapping_add(f1.wrapping_mul(d1)).wrapping_add(f2)
275}
276fn split(hash: u64) -> (u32, u32, u32) {
277 let bits = 21;
278 let mask = (1 << bits) - 1;
279 (
280 (hash & mask) as u32,
281 ((hash >> bits) & mask) as u32,
282 ((hash >> (2 * bits)) & mask) as u32,
283 )
284}
285
286fn character_by_alias(name: &[u8]) -> Option<char> {
288 ALIASES.get(name).copied()
289}
290
291pub fn character(search_name: &str) -> Option<char> {
309 let mut buf = [0; MAX_NAME_LENGTH + 1];
312 for (place, byte) in buf.iter_mut().zip(search_name.bytes()) {
313 *place = byte.to_ascii_uppercase();
314 }
315 let search_name = buf.get(..search_name.len())?;
316
317 if search_name.starts_with(HANGUL_SYLLABLE_PREFIX.as_bytes()) {
319 let remaining = &search_name[HANGUL_SYLLABLE_PREFIX.len()..];
320 let (choseong, remaining) = jamo::slice_shift_choseong(remaining);
321 let (jungseong, remaining) = jamo::slice_shift_jungseong(remaining);
322 let (jongseong, remaining) = jamo::slice_shift_jongseong(remaining);
323 match (choseong, jungseong, jongseong, remaining) {
324 (Some(choseong), Some(jungseong), Some(jongseong), b"") => {
325 let c = 0xac00 + (choseong * 21 + jungseong) * 28 + jongseong;
326 return char::from_u32(c);
327 }
328 (_, _, _, _) => {
329 return None;
332 }
333 }
334 }
335
336 if search_name.starts_with(CJK_UNIFIED_IDEOGRAPH_PREFIX.as_bytes()) {
338 let remaining = &search_name[CJK_UNIFIED_IDEOGRAPH_PREFIX.len()..];
339 if remaining.len() > 5 {
340 return None;
341 } let mut v = 0u32;
344 for &c in remaining {
345 v = match c {
346 b'0'..=b'9' => (v << 4) | (c - b'0') as u32,
347 b'A'..=b'F' => (v << 4) | (c - b'A' + 10) as u32,
348 _ => return None,
349 }
350 }
351 let ch = char::from_u32(v)?;
352
353 if is_cjk_unified_ideograph(ch) {
355 return Some(ch);
356 } else {
357 return None;
360 }
361 }
362
363 let (g, f1, f2) = split(fnv_hash(search_name.iter().copied()));
365 let (d1, d2) = generated_phf::NAME2CODE_DISP[g as usize % generated_phf::NAME2CODE_DISP.len()];
367
368 let idx = displace(f1, f2, d1 as u32, d2 as u32) as usize;
370 let codepoint = generated_phf::NAME2CODE_CODE[idx % generated_phf::NAME2CODE_CODE.len()];
372
373 let maybe_name = match name(codepoint) {
379 None => {
380 if true {
381 debug_assert!(false)
382 }
383 return character_by_alias(search_name);
384 }
385 Some(name) => name,
386 };
387
388 let mut passed_name = search_name;
391 for part in maybe_name {
392 let part = part.as_bytes();
393 let part_l = part.len();
394 if passed_name.len() < part_l || &passed_name[..part_l] != part {
395 return character_by_alias(search_name);
396 }
397 passed_name = &passed_name[part_l..]
398 }
399
400 Some(codepoint)
401}
402
403#[cfg(test)]
404mod tests {
405 use super::*;
406 use rand::{
407 distr::{Distribution, StandardUniform},
408 prelude::{SeedableRng, StdRng},
409 };
410 use std::char;
411 use std::prelude::v1::*;
412
413 extern crate test;
414
415 use test::bench::Bencher;
416
417 static DATA: &str = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/data/UnicodeData.txt"));
418
419 #[test]
420 fn exhaustive() {
421 fn negative_range(from: u32, to: u32) {
424 for c in (from..to).filter_map(char::from_u32) {
425 if !is_cjk_unified_ideograph(c) && !jamo::is_hangul_syllable(c) {
426 let n = name(c);
427 assert!(
428 n.is_none(),
429 "{} ({}) shouldn't have a name but is called {}",
430 c,
431 c as u32,
432 n.unwrap()
433 );
434 }
435 }
436 }
437
438 let mut last = 0;
439 for line in DATA.lines() {
440 let mut it = line.split(';');
441
442 let raw_c = it.next();
443 let c = match char::from_u32(
444 raw_c.and_then(|s| u32::from_str_radix(s, 16).ok()).unwrap(),
445 ) {
446 Some(c) => c,
447 None => continue,
448 };
449
450 let n = it.next().unwrap();
451 if n.starts_with("<") {
452 continue;
453 }
454
455 let computed_n = name(c).unwrap();
456 let n_str = computed_n.to_string();
457 assert_eq!(n_str, n.to_string());
458 assert_eq!(computed_n.len(), n_str.len());
459
460 let (hint_low, hint_high) = computed_n.size_hint();
461 let number_of_parts = computed_n.count();
462 assert_eq!(hint_low, number_of_parts);
463 assert_eq!(hint_high, Some(number_of_parts));
464
465 assert_eq!(character(n), Some(c));
466 assert_eq!(character(&n.to_ascii_lowercase()), Some(c));
467
468 negative_range(last, c as u32);
469 last = c as u32 + 1;
470 }
471 negative_range(last, 0x10FFFF + 1)
472 }
473
474 #[test]
475 fn name_to_string() {
476 let n = name('a').unwrap();
477 assert_eq!(n.to_string(), "LATIN SMALL LETTER A".to_string());
478 let n = name('🁣').unwrap();
479 assert_eq!(n.to_string(), "DOMINO TILE VERTICAL-00-00".to_string());
480 }
481
482 #[test]
483 fn character_negative() {
484 let long_name = "x".repeat(100);
485 assert!(long_name.len() > MAX_NAME_LENGTH); let names = ["", "x", "öäå", "SPAACE", &long_name];
487 for &n in names.iter() {
488 assert_eq!(character(n), None);
489 }
490 }
491
492 #[test]
493 fn name_hangul_syllable() {
494 assert_eq!(
495 name('\u{ac00}').map(|s| s.to_string()),
496 Some("HANGUL SYLLABLE GA".to_string())
497 ); assert_eq!(
499 name('\u{bdc1}').map(|s| s.to_string()),
500 Some("HANGUL SYLLABLE BWELG".to_string())
501 );
502 assert_eq!(
503 name('\u{d7a3}').map(|s| s.to_string()),
504 Some("HANGUL SYLLABLE HIH".to_string())
505 ); }
507
508 #[test]
509 fn character_hangul_syllable() {
510 assert_eq!(character("HANGUL SYLLABLE GA"), Some('\u{ac00}'));
511 assert_eq!(character("HANGUL SYLLABLE BWELG"), Some('\u{bdc1}'));
512 assert_eq!(character("HANGUL SYLLABLE HIH"), Some('\u{d7a3}'));
513 assert_eq!(character("HANGUL SYLLABLE BLAH"), None);
514 }
515
516 #[test]
517 fn cjk_unified_ideograph_exhaustive() {
518 for &(lo, hi) in generated::CJK_IDEOGRAPH_RANGES.iter() {
519 for x in lo as u32..=hi as u32 {
520 let c = char::from_u32(x).unwrap();
521
522 let real_name = format!("CJK UNIFIED IDEOGRAPH-{:X}", x);
523 let lower_real_name = format!("CJK UNIFIED IDEOGRAPH-{:x}", x);
524 assert_eq!(character(&real_name), Some(c));
525 assert_eq!(character(&lower_real_name), Some(c));
526
527 assert_eq!(name(c).map(|s| s.to_string()), Some(real_name));
528 }
529 }
530 }
531 #[test]
532 fn name_cjk_unified_ideograph() {
533 assert_eq!(
534 name('\u{4e00}').map(|s| s.to_string()),
535 Some("CJK UNIFIED IDEOGRAPH-4E00".to_string())
536 ); assert_eq!(
538 name('\u{9fcc}').map(|s| s.to_string()),
539 Some("CJK UNIFIED IDEOGRAPH-9FCC".to_string())
540 ); assert_eq!(
542 name('\u{20000}').map(|s| s.to_string()),
543 Some("CJK UNIFIED IDEOGRAPH-20000".to_string())
544 ); assert_eq!(
546 name('\u{2a6d6}').map(|s| s.to_string()),
547 Some("CJK UNIFIED IDEOGRAPH-2A6D6".to_string())
548 );
549 assert_eq!(
550 name('\u{2a700}').map(|s| s.to_string()),
551 Some("CJK UNIFIED IDEOGRAPH-2A700".to_string())
552 );
553 assert_eq!(
554 name('\u{2b81d}').map(|s| s.to_string()),
555 Some("CJK UNIFIED IDEOGRAPH-2B81D".to_string())
556 ); }
558
559 #[test]
560 fn character_cjk_unified_ideograph() {
561 assert_eq!(character("CJK UNIFIED IDEOGRAPH-4E00"), Some('\u{4e00}'));
562 assert_eq!(character("CJK UNIFIED IDEOGRAPH-9FCC"), Some('\u{9fcc}'));
563 assert_eq!(character("CJK UNIFIED IDEOGRAPH-20000"), Some('\u{20000}'));
564 assert_eq!(character("CJK UNIFIED IDEOGRAPH-2A6D6"), Some('\u{2a6d6}'));
565 assert_eq!(character("CJK UNIFIED IDEOGRAPH-2A700"), Some('\u{2a700}'));
566 assert_eq!(character("CJK UNIFIED IDEOGRAPH-2B81D"), Some('\u{2b81d}'));
567 assert_eq!(character("CJK UNIFIED IDEOGRAPH-"), None);
568 assert_eq!(character("CJK UNIFIED IDEOGRAPH-!@#$"), None);
569 assert_eq!(character("CJK UNIFIED IDEOGRAPH-1234"), None);
570 assert_eq!(character("CJK UNIFIED IDEOGRAPH-EFGH"), None);
571 assert_eq!(character("CJK UNIFIED IDEOGRAPH-12345"), None);
572 assert_eq!(character("CJK UNIFIED IDEOGRAPH-2A6FF"), None); assert_eq!(character("CJK UNIFIED IDEOGRAPH-2A6FF"), None);
574 }
575
576 #[test]
577 fn character_by_alias() {
578 assert_eq!(super::character_by_alias(b"NEW LINE"), Some('\n'));
579 assert_eq!(super::character_by_alias(b"BACKSPACE"), Some('\u{8}'));
580 assert_eq!(super::character_by_alias(b"NOT AN ALIAS"), None);
581 }
582
583 #[bench]
584 fn name_basic(b: &mut Bencher) {
585 b.iter(|| {
586 for s in name('ö').unwrap() {
587 test::black_box(s);
588 }
589 })
590 }
591
592 #[bench]
593 fn character_basic(b: &mut Bencher) {
594 b.iter(|| character("LATIN SMALL LETTER O WITH DIAERESIS"));
595 }
596
597 #[bench]
598 fn name_10000_invalid(b: &mut Bencher) {
599 let mut rng = StdRng::seed_from_u64(0x12345678);
601 let chars: Vec<char> = StandardUniform
602 .sample_iter(&mut rng)
603 .take(10000)
604 .filter_map(|c| match c {
605 c if name(c).is_none() => Some(c),
606 _ => None,
607 })
608 .collect();
609
610 b.iter(|| {
611 for &c in chars.iter() {
612 assert!(name(c).is_none());
613 }
614 })
615 }
616
617 #[bench]
618 fn name_all_valid(b: &mut Bencher) {
619 let chars = (0u32..0x10FFFF)
620 .filter_map(|x| match char::from_u32(x) {
621 Some(c) if name(c).is_some() => Some(c),
622 _ => None,
623 })
624 .collect::<Vec<char>>();
625
626 b.iter(|| {
627 for c in chars.iter() {
628 for s in name(*c).unwrap() {
629 test::black_box(s);
630 }
631 }
632 });
633 }
634
635 #[bench]
636 fn character_10000(b: &mut Bencher) {
637 let mut rng = StdRng::seed_from_u64(0x12345678);
639
640 let names: Vec<_> = StandardUniform
641 .sample_iter(&mut rng)
642 .take(10000)
643 .filter_map(name)
644 .map(|name| name.to_string())
645 .collect();
646
647 b.iter(|| {
648 for n in names.iter() {
649 test::black_box(character(n));
650 }
651 })
652 }
653}