Skip to main content

unicode_names2/
lib.rs

1//! Convert between characters and their standard names.
2//!
3//! This crate provides two functions for mapping from a `char` to the
4//! name given by the Unicode standard (17.0). The tables are heavily
5//! compressed, but still large (500KB), and still offer efficient
6//! `O(1)` look-ups in both directions (more precisely, `O(length of
7//! name)`).
8//!
9//! ```rust
10//!     println!("☃ is called {:?}", unicode_names2::name('☃')); // SNOWMAN
11//!     println!("{:?} is happy", unicode_names2::character("white smiling face")); // ☺
12//!     // (NB. case insensitivity)
13//! ```
14//!
15//! [**Source**](https://github.com/ProgVal/unicode_names2).
16//!
17//! # Macros
18//!
19//! The associated `unicode_names2_macros` crate provides two macros
20//! for converting at compile-time, giving named literals similar to
21//! Python's `"\N{...}"`.
22//!
23//! - `named_char!(name)` takes a single string `name` and creates a
24//!   `char` literal.
25//! - `named!(string)` takes a string and replaces any `\\N{name}`
26//!   sequences with the character with that name. NB. String escape
27//!   sequences cannot be customised, so the extra backslash (or a raw
28//!   string) is required, unless you use a raw string.
29//!
30//! ```rust,ignore
31//! #![feature(proc_macro_hygiene)]
32//!
33//! #[macro_use]
34//! extern crate unicode_names2_macros;
35//!
36//! fn main() {
37//!     let x: char = named_char!("snowman");
38//!     assert_eq!(x, '☃');
39//!
40//!     let y: &str = named!("foo bar \\N{BLACK STAR} baz qux");
41//!     assert_eq!(y, "foo bar ★ baz qux");
42//!
43//!     let y: &str = named!(r"foo bar \N{BLACK STAR} baz qux");
44//!     assert_eq!(y, "foo bar ★ baz qux");
45//! }
46//! ```
47
48#![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
64/// A map of unicode aliases to their corresponding values.
65/// Generated in generator
66static 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/// An iterator over the components of a code point's name, it also
81/// implements `Show`.
82///
83/// The size hint is exact for the number of pieces, but iterates
84/// (although iteration is cheap and all names are short).
85#[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    // the longest character is 0x10FFFF
103    data: [u8; 6],
104}
105#[derive(Copy)]
106struct Hangul {
107    emit_prefix: bool,
108    idx: u8,
109    // stores the choseong, jungseong, jongseong syllable numbers (in
110    // that order)
111    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    /// The number of bytes in the name.
127    ///
128    /// All names are plain ASCII, so this is also the number of
129    /// Unicode codepoints and the number of graphemes.
130    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                // we're a CJK unified ideograph
143                if state.emit_prefix {
144                    state.emit_prefix = false;
145                    return Some(CJK_UNIFIED_IDEOGRAPH_PREFIX);
146                }
147                // run until we've run out of array: the construction
148                // of the data means this is exactly when we have
149                // finished emitting the number.
150                state
151                    .data
152                    .get(state.idx as usize)
153                    // (avoid conflicting mutable borrow problems)
154                    .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                    // progressively walk through the syllables
170                    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        // we can estimate exactly by just iterating and summing up.
179        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
196/// Find the name of `c`, or `None` if `c` has no name.
197///
198/// The return value is an iterator that yields `&str` components of
199/// the name successively (including spaces and hyphens). It
200/// implements `Show`, and thus can be used naturally to build
201/// `String`s, or be printed, etc.
202///
203/// # Example
204///
205/// ```rust
206/// assert_eq!(unicode_names2::name('a').map(|n| n.to_string()),
207///            Some("LATIN SMALL LETTER A".to_string()));
208/// assert_eq!(unicode_names2::name('\u{2605}').map(|n| n.to_string()),
209///            Some("BLACK STAR".to_string()));
210/// assert_eq!(unicode_names2::name('☃').map(|n| n.to_string()),
211///            Some("SNOWMAN".to_string()));
212///
213/// // control code
214/// assert!(unicode_names2::name('\x00').is_none());
215/// // unassigned
216/// assert!(unicode_names2::name('\u{10FFFF}').is_none());
217/// ```
218pub 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            // write the hex number out right aligned in this array.
228            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                // this would be incorrect if U+0000 was CJK unified
233                // ideograph, but it's not, so it's fine.
234                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            // maybe it is a hangul syllable?
250            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
286/// Get alias value from alias name, returns `None` if the alias is not found.
287fn character_by_alias(name: &[u8]) -> Option<char> {
288    ALIASES.get(name).copied()
289}
290
291/// Find the character called `name`, or `None` if no such character
292/// exists.
293///
294/// This searches case-insensitively.
295///
296/// # Example
297///
298/// ```rust
299/// assert_eq!(unicode_names2::character("LATIN SMALL LETTER A"), Some('a'));
300/// assert_eq!(unicode_names2::character("latin SMALL letter A"), Some('a'));
301/// assert_eq!(unicode_names2::character("latin small letter a"), Some('a'));
302/// assert_eq!(unicode_names2::character("BLACK STAR"), Some('★'));
303/// assert_eq!(unicode_names2::character("SNOWMAN"), Some('☃'));
304/// assert_eq!(unicode_names2::character("BACKSPACE"), Some('\x08'));
305///
306/// assert_eq!(unicode_names2::character("nonsense"), None);
307/// ```
308pub fn character(search_name: &str) -> Option<char> {
309    // + 1 so that we properly handle the case when `name` has a
310    // prefix of the longest name, but isn't exactly equal.
311    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    // try `HANGUL SYLLABLE <choseong><jungseong><jongseong>`
318    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                // there are no other names starting with `HANGUL SYLLABLE `
330                // (verified by `generator/...`).
331                return None;
332            }
333        }
334    }
335
336    // try `CJK UNIFIED IDEOGRAPH-<digits>`
337    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        } // avoid overflow
342
343        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        // check if the resulting code is indeed in the known ranges
354        if is_cjk_unified_ideograph(ch) {
355            return Some(ch);
356        } else {
357            // there are no other names starting with `CJK UNIFIED IDEOGRAPH-`
358            // (verified by `src/generate.py`).
359            return None;
360        }
361    }
362
363    // get the parts of the hash...
364    let (g, f1, f2) = split(fnv_hash(search_name.iter().copied()));
365    // ...and the appropriate displacements...
366    let (d1, d2) = generated_phf::NAME2CODE_DISP[g as usize % generated_phf::NAME2CODE_DISP.len()];
367
368    // ...to find the right index...
369    let idx = displace(f1, f2, d1 as u32, d2 as u32) as usize;
370    // ...for looking up the codepoint.
371    let codepoint = generated_phf::NAME2CODE_CODE[idx % generated_phf::NAME2CODE_CODE.len()];
372
373    // Now check that this is actually correct. Since this is a
374    // perfect hash table, valid names map precisely to their code
375    // point (and invalid names map to anything), so we only need to
376    // check the name for this codepoint matches the input and we know
377    // everything. (i.e. no need for probing)
378    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    // run through the parts of the name, matching them against the
389    // parts of the input.
390    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        // check that gaps have no names (these are unassigned/control
422        // codes).
423        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); // Otherwise this test is pointless
486        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        ); // first
498        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        ); // last
506    }
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        ); // first in BMP
537        assert_eq!(
538            name('\u{9fcc}').map(|s| s.to_string()),
539            Some("CJK UNIFIED IDEOGRAPH-9FCC".to_string())
540        ); // last in BMP (as of 6.1)
541        assert_eq!(
542            name('\u{20000}').map(|s| s.to_string()),
543            Some("CJK UNIFIED IDEOGRAPH-20000".to_string())
544        ); // first in SIP
545        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        ); // last in SIP (as of 6.0)
557    }
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); // between Ext B and Ext C
573        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        // be consistent across runs, but avoid sequential/caching.
600        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        // be consistent across runs, but avoid sequential/caching.
638        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}