1use crate::{constants::ALL_CATEGORIES, error};
2use core::{
3 fmt,
4 ops::{BitOr, BitOrAssign},
5 str::FromStr,
6};
7use UnicodeCategory::*;
8
9#[derive(Debug, Eq, PartialEq, Copy, Clone, Hash)]
11pub enum UnicodeCategory {
12 Pe,
14 Pc,
16 Cc,
18 Sc,
20 Pd,
22 Nd,
24 Me,
26 Pf,
28 Cf,
30 Pi,
32 Nl,
34 Zl,
36 Ll,
38 Sm,
40 Lm,
42 Sk,
44 Mn,
46 Ps,
48 Lo,
50 No,
52 Po,
54 So,
56 Zp,
58 Co,
60 Zs,
62 Mc,
64 Cs,
66 Lt,
68 Cn,
70 Lu,
72}
73
74impl FromStr for UnicodeCategory {
75 type Err = error::Error;
76
77 fn from_str(s: &str) -> Result<Self, Self::Err> {
78 Ok(match s {
79 "Pe" => Pe,
80 "Pc" => Pc,
81 "Cc" => Cc,
82 "Sc" => Sc,
83 "Pd" => Pd,
84 "Nd" => Nd,
85 "Me" => Me,
86 "Pf" => Pf,
87 "Cf" => Cf,
88 "Pi" => Pi,
89 "Nl" => Nl,
90 "Zl" => Zl,
91 "Ll" => Ll,
92 "Sm" => Sm,
93 "Lm" => Lm,
94 "Sk" => Sk,
95 "Mn" => Mn,
96 "Ps" => Ps,
97 "Lo" => Lo,
98 "No" => No,
99 "Po" => Po,
100 "So" => So,
101 "Zp" => Zp,
102 "Co" => Co,
103 "Zs" => Zs,
104 "Mc" => Mc,
105 "Cs" => Cs,
106 "Lt" => Lt,
107 "Cn" => Cn,
108 "Lu" => Lu,
109 _ => return Err(Self::Err::InvalidCategory(s.to_owned().into_boxed_str())),
110 })
111 }
112}
113
114impl UnicodeCategory {
115 pub const L: UnicodeCategorySet = UnicodeCategorySet(
117 1 << Ll as u32 | 1 << Lm as u32 | 1 << Lo as u32 | 1 << Lt as u32 | 1 << Lu as u32,
118 );
119 pub const M: UnicodeCategorySet =
121 UnicodeCategorySet(1 << Mc as u32 | 1 << Me as u32 | 1 << Mn as u32);
122 pub const N: UnicodeCategorySet =
124 UnicodeCategorySet(1 << Nd as u32 | 1 << Nl as u32 | 1 << No as u32);
125 pub const P: UnicodeCategorySet = UnicodeCategorySet(
127 1 << Pc as u32
128 | 1 << Pd as u32
129 | 1 << Pe as u32
130 | 1 << Pf as u32
131 | 1 << Pi as u32
132 | 1 << Po as u32
133 | 1 << Ps as u32,
134 );
135 pub const S: UnicodeCategorySet =
137 UnicodeCategorySet(1 << Sc as u32 | 1 << Sk as u32 | 1 << Sm as u32 | 1 << So as u32);
138 pub const Z: UnicodeCategorySet =
140 UnicodeCategorySet(1 << Zp as u32 | 1 << Zs as u32 | 1 << Zl as u32);
141 pub const C: UnicodeCategorySet = UnicodeCategorySet(
143 1 << Cc as u32 | 1 << Cf as u32 | 1 << Cn as u32 | 1 << Co as u32 | 1 << Cs as u32,
144 );
145 pub const CLOSE_PUNCTUATION: UnicodeCategory = Pe;
148 pub const CONNECTOR_PUNCTUATION: UnicodeCategory = Pc;
150 pub const CONTROL: UnicodeCategory = Cc;
152 pub const CURRENCY_SYMBOL: UnicodeCategory = Sc;
154 pub const DASH_PUNCTUATION: UnicodeCategory = Pd;
156 pub const DECIMAL_NUMBER: UnicodeCategory = Nd;
158 pub const ENCLOSING_MARK: UnicodeCategory = Me;
160 pub const FINAL_PUNCTUATION: UnicodeCategory = Pf;
162 pub const FORMAT: UnicodeCategory = Cf;
164 pub const INITIAL_PUNCTUATION: UnicodeCategory = Pi;
166 pub const LETTER_NUMBER: UnicodeCategory = Nl;
168 pub const LINE_SEPARATOR: UnicodeCategory = Zl;
170 pub const LOWERCASE_LETTER: UnicodeCategory = Ll;
172 pub const MATH_SYMBOL: UnicodeCategory = Sm;
174 pub const MODIFIER_LETTER: UnicodeCategory = Lm;
176 pub const MODIFIER_SYMBOL: UnicodeCategory = Sk;
178 pub const NONSPACING_MARK: UnicodeCategory = Mn;
180 pub const OPEN_PUNCTUATION: UnicodeCategory = Ps;
182 pub const OTHER_LETTER: UnicodeCategory = Lo;
184 pub const OTHER_NUMBER: UnicodeCategory = No;
186 pub const OTHER_PUNCTUATION: UnicodeCategory = Po;
188 pub const OTHER_SYMBOL: UnicodeCategory = So;
190 pub const PARAGRAPH_SEPARATOR: UnicodeCategory = Zp;
192 pub const PRIVATE_USE: UnicodeCategory = Co;
194 pub const SPACE_SEPARATOR: UnicodeCategory = Zs;
196 pub const SPACING_MARK: UnicodeCategory = Mc;
198 pub const SURROGATE: UnicodeCategory = Cs;
200 pub const TITLECASE_LETTER: UnicodeCategory = Lt;
202 pub const UNASSIGNED: UnicodeCategory = Cn;
204 pub const UPPERCASE_LETTER: UnicodeCategory = Lu;
206
207 #[must_use]
209 pub(crate) const fn from_index(index: u8) -> Option<UnicodeCategory> {
210 Some(match index {
211 0 => Pe,
212 1 => Pc,
213 2 => Cc,
214 3 => Sc,
215 4 => Pd,
216 5 => Nd,
217 6 => Me,
218 7 => Pf,
219 8 => Cf,
220 9 => Pi,
221 10 => Nl,
222 11 => Zl,
223 12 => Ll,
224 13 => Sm,
225 14 => Lm,
226 15 => Sk,
227 16 => Mn,
228 17 => Ps,
229 18 => Lo,
230 19 => No,
231 20 => Po,
232 21 => So,
233 22 => Zp,
234 23 => Co,
235 24 => Zs,
236 25 => Mc,
237 26 => Cs,
238 27 => Lt,
239 28 => Cn,
240 29 => Lu,
241 _ => return None,
242 })
243 }
244
245 #[must_use]
247 pub(crate) const fn abbrev_rank(self) -> u8 {
248 match self {
249 Cc => 0,
250 Cf => 1,
251 Cn => 2,
252 Co => 3,
253 Cs => 4,
254 Ll => 5,
255 Lm => 6,
256 Lo => 7,
257 Lt => 8,
258 Lu => 9,
259 Mc => 10,
260 Me => 11,
261 Mn => 12,
262 Nd => 13,
263 Nl => 14,
264 No => 15,
265 Pc => 16,
266 Pd => 17,
267 Pe => 18,
268 Pf => 19,
269 Pi => 20,
270 Po => 21,
271 Ps => 22,
272 Sc => 23,
273 Sk => 24,
274 Sm => 25,
275 So => 26,
276 Zl => 27,
277 Zp => 28,
278 Zs => 29,
279 }
280 }
281
282 #[must_use]
284 pub const fn as_str(self) -> &'static str {
285 match self {
286 Pe => "Pe",
287 Pc => "Pc",
288 Cc => "Cc",
289 Sc => "Sc",
290 Pd => "Pd",
291 Nd => "Nd",
292 Me => "Me",
293 Pf => "Pf",
294 Cf => "Cf",
295 Pi => "Pi",
296 Nl => "Nl",
297 Zl => "Zl",
298 Ll => "Ll",
299 Sm => "Sm",
300 Lm => "Lm",
301 Sk => "Sk",
302 Mn => "Mn",
303 Ps => "Ps",
304 Lo => "Lo",
305 No => "No",
306 Po => "Po",
307 So => "So",
308 Zp => "Zp",
309 Co => "Co",
310 Zs => "Zs",
311 Mc => "Mc",
312 Cs => "Cs",
313 Lt => "Lt",
314 Cn => "Cn",
315 Lu => "Lu",
316 }
317 }
318}
319
320impl fmt::Display for UnicodeCategory {
321 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
322 f.write_str(self.as_str())
323 }
324}
325
326#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)]
328pub struct UnicodeCategorySet(u32);
329
330impl UnicodeCategorySet {
331 #[inline]
333 #[must_use]
334 pub const fn new() -> Self {
335 Self(0)
336 }
337 #[inline]
339 #[must_use]
340 pub const fn all() -> Self {
341 Self(ALL_CATEGORIES)
342 }
343 #[inline]
345 #[must_use]
346 pub(crate) const fn from_value_unchecked(value: u32) -> Self {
347 Self(value)
348 }
349 #[inline]
351 pub fn add(&mut self, category: UnicodeCategory) {
352 self.set(category as u8);
353 }
354 #[inline]
356 pub fn remove(&mut self, category: UnicodeCategory) {
357 self.unset(category as u8);
358 }
359 #[inline]
361 #[must_use]
362 pub const fn contains(self, category: UnicodeCategory) -> bool {
363 self.is_set(category as u8)
364 }
365 #[inline]
367 #[must_use]
368 pub const fn len(self) -> usize {
369 self.0.count_ones() as usize
370 }
371 #[inline]
373 #[must_use]
374 pub const fn is_empty(self) -> bool {
375 self.0 == 0
376 }
377 #[inline]
379 #[must_use]
380 pub const fn into_value(self) -> u32 {
381 self.0
382 }
383 #[inline]
385 #[must_use]
386 pub const fn iter(self) -> Iter {
387 Iter { data: self }
388 }
389 #[inline]
391 #[allow(clippy::arithmetic_side_effects)]
392 pub(crate) fn set(&mut self, index: u8) {
393 self.0 |= 1 << index;
394 }
395 #[inline]
397 #[allow(clippy::arithmetic_side_effects)]
398 pub(crate) fn unset(&mut self, index: u8) {
399 self.0 &= !(1 << index);
400 }
401 #[inline]
403 #[allow(clippy::arithmetic_side_effects)]
404 const fn is_set(self, index: u8) -> bool {
405 self.0 & (1 << index) != 0
406 }
407}
408
409impl Default for UnicodeCategorySet {
410 #[inline]
411 fn default() -> Self {
412 UnicodeCategorySet::new()
413 }
414}
415
416impl fmt::Display for UnicodeCategorySet {
417 #[allow(clippy::arithmetic_side_effects)]
419 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
420 let len = self.len();
421 for (idx, category) in self.iter().enumerate() {
422 f.write_str(category.as_str())?;
423 if idx + 1 != len {
424 f.write_str(", ")?;
425 }
426 }
427 Ok(())
428 }
429}
430
431impl BitOr for UnicodeCategory {
432 type Output = UnicodeCategorySet;
433
434 #[inline]
436 #[allow(clippy::arithmetic_side_effects)]
437 fn bitor(self, rhs: Self) -> Self::Output {
438 UnicodeCategorySet(1 << self as u8 | 1 << rhs as u8)
439 }
440}
441impl BitOr<UnicodeCategorySet> for UnicodeCategory {
442 type Output = UnicodeCategorySet;
443
444 #[inline]
445 fn bitor(self, rhs: UnicodeCategorySet) -> Self::Output {
446 rhs | self
448 }
449}
450
451impl BitOr<UnicodeCategory> for UnicodeCategorySet {
452 type Output = Self;
453
454 #[inline]
456 #[allow(clippy::arithmetic_side_effects)]
457 fn bitor(self, rhs: UnicodeCategory) -> Self::Output {
458 Self(self.into_value() | 1 << rhs as u8)
459 }
460}
461
462impl BitOr<UnicodeCategorySet> for UnicodeCategorySet {
463 type Output = Self;
464
465 #[inline]
466 fn bitor(self, rhs: UnicodeCategorySet) -> Self::Output {
467 Self(self.into_value() | rhs.into_value())
468 }
469}
470
471impl BitOrAssign<UnicodeCategorySet> for UnicodeCategorySet {
472 #[inline]
473 fn bitor_assign(&mut self, rhs: UnicodeCategorySet) {
474 self.0 |= rhs.into_value();
475 }
476}
477
478impl BitOrAssign<UnicodeCategory> for UnicodeCategorySet {
479 #[inline]
480 fn bitor_assign(&mut self, rhs: UnicodeCategory) {
481 self.add(rhs);
482 }
483}
484
485#[derive(Debug)]
486pub struct Iter {
487 data: UnicodeCategorySet,
488}
489
490impl Iterator for Iter {
491 type Item = UnicodeCategory;
492
493 fn next(&mut self) -> Option<Self::Item> {
494 #[allow(clippy::cast_possible_truncation)]
496 let index = self.data.0.trailing_zeros() as u8;
497 let category = UnicodeCategory::from_index(index)?;
498 self.data.unset(index);
499 Some(category)
500 }
501}
502
503impl ExactSizeIterator for Iter {
504 #[inline]
505 fn len(&self) -> usize {
506 self.data.len()
507 }
508}
509
510impl From<UnicodeCategory> for UnicodeCategorySet {
511 #[inline]
513 #[allow(clippy::arithmetic_side_effects)]
514 fn from(category: UnicodeCategory) -> Self {
515 Self::from_value_unchecked(1 << category as u8)
516 }
517}
518
519impl From<UnicodeCategory> for Option<UnicodeCategorySet> {
520 #[inline]
521 fn from(category: UnicodeCategory) -> Self {
522 Some(category.into())
523 }
524}
525
526#[inline]
528#[must_use]
529pub const fn merge(
530 include: Option<UnicodeCategorySet>,
531 exclude: UnicodeCategorySet,
532) -> UnicodeCategorySet {
533 if let Some(include) = include {
534 if include.is_empty() {
535 include
537 } else {
538 UnicodeCategorySet::from_value_unchecked(
539 (ALL_CATEGORIES ^ exclude.into_value()) & include.into_value(),
540 )
541 }
542 } else {
543 UnicodeCategorySet::from_value_unchecked(ALL_CATEGORIES ^ exclude.into_value())
544 }
545}
546
547pub fn as_general_categories<I, S>(
554 categories: I,
555 version: crate::UnicodeVersion,
556) -> Result<Vec<UnicodeCategory>, error::Error>
557where
558 I: IntoIterator<Item = S>,
559 S: AsRef<str>,
560{
561 let mut wanted = UnicodeCategorySet::new();
562 for category in categories {
563 let set = match category.as_ref() {
564 "L" => UnicodeCategory::L,
565 "M" => UnicodeCategory::M,
566 "N" => UnicodeCategory::N,
567 "P" => UnicodeCategory::P,
568 "S" => UnicodeCategory::S,
569 "Z" => UnicodeCategory::Z,
570 "C" => UnicodeCategory::C,
571 other => other.parse::<UnicodeCategory>()?.into(),
572 };
573 wanted |= set;
574 }
575 Ok(version
576 .normalized_categories()
577 .into_iter()
578 .filter(|c| wanted.contains(*c))
579 .collect())
580}
581
582#[cfg(test)]
583mod tests {
584 use super::*;
585 use crate::UnicodeVersion;
586 use std::{
587 collections::hash_map::DefaultHasher,
588 hash::{Hash, Hasher},
589 };
590 use test_case::test_case;
591
592 #[test]
593 fn test_category_from_str_error() {
594 assert_eq!(
595 UnicodeCategory::from_str("wrong")
596 .expect_err("Should fail")
597 .to_string(),
598 "'wrong' is not a valid Unicode category"
599 );
600 }
601
602 #[test]
603 #[allow(clippy::clone_on_copy)]
604 fn test_category_traits() {
605 let mut hasher = DefaultHasher::new();
606 Ll.hash(&mut hasher);
607 let _ = hasher.finish();
608 let _ = Ll.clone();
609 assert_eq!(format!("{Ll:?}"), "Ll");
610 }
611
612 #[test]
613 fn test_single_letter_categories() {
614 assert_eq!(UnicodeCategory::L, Ll | Lm | Lo | Lt | Lu);
615 }
616
617 #[test]
618 fn test_set_display() {
619 assert_eq!(UnicodeCategory::L.to_string(), "Ll, Lm, Lo, Lt, Lu");
620 }
621
622 #[test]
623 fn test_set_add() {
624 let mut set = UnicodeCategorySet::new();
625 assert!(set.is_empty());
626 set.add(Ll);
627 assert!(set.contains(Ll));
628 assert_eq!(set.len(), 1);
629 }
630
631 #[test]
632 fn test_set_remove() {
633 let mut set = UnicodeCategorySet::all();
634 assert!(set.contains(Ll));
635 set.remove(Ll);
636 assert!(!set.contains(Ll));
637 }
638
639 #[test]
640 #[allow(clippy::clone_on_copy)]
641 fn test_category_set_traits() {
642 let set = UnicodeCategory::L;
643 let mut hasher = DefaultHasher::new();
644 set.hash(&mut hasher);
645 let _ = hasher.finish();
646 let _ = set.clone();
647 assert_eq!(format!("{set:?}"), "UnicodeCategorySet(671371264)");
648 }
649
650 #[test]
651 fn test_iter_traits() {
652 let set = UnicodeCategory::L;
653 let iter = set.iter();
654 assert_eq!(
655 format!("{iter:?}"),
656 "Iter { data: UnicodeCategorySet(671371264) }"
657 );
658 }
659
660 #[test]
661 fn test_bit_or() {
662 assert_eq!(Ll | UnicodeCategorySet::new(), Ll.into());
663 assert_eq!(
664 UnicodeCategory::L | UnicodeCategory::C,
665 Ll | Lm | Lo | Lt | Lu | Cs | Cc | Cf | Cn | Co
666 );
667 let mut set = UnicodeCategorySet::new();
668 set |= Ll;
669 set |= UnicodeCategory::C;
670 assert_eq!(set, Ll | Cs | Cc | Cf | Cn | Co);
671 }
672
673 #[test]
674 fn test_set_iter() {
675 let all_categories = UnicodeCategorySet::all();
676 assert_eq!(all_categories.iter().len(), all_categories.len());
677 let mut set = UnicodeCategorySet::new();
678 for category in all_categories.iter() {
679 let name = format!("{category}");
680 assert_eq!(
681 UnicodeCategory::from_str(&name).expect("Invalid category"),
682 category
683 );
684 set.add(category);
685 }
686 assert_eq!(all_categories, set);
687 }
688
689 #[test]
690 fn test_set_default() {
691 assert_eq!(UnicodeCategorySet::default(), UnicodeCategorySet::new());
692 }
693
694 #[test]
695 fn test_set_option_from_category() {
696 let set: Option<UnicodeCategorySet> = Ll.into();
697 assert!(set.is_some());
698 assert_eq!(set.expect("Unexpected `None`"), Ll.into());
699 }
700
701 #[test_case(Some(Lu | Me | Cs | So), So.into(), Lu | Me | Cs)]
702 #[test_case(None, UnicodeCategory::L | UnicodeCategory::M | UnicodeCategory::N | UnicodeCategory::P | UnicodeCategory::S, UnicodeCategory::Z | UnicodeCategory::C)]
703 #[test_case(
704 Some(UnicodeCategorySet::new()),
705 UnicodeCategorySet::new(),
706 UnicodeCategorySet::new()
707 )]
708 fn test_category_merge(
709 include: Option<UnicodeCategorySet>,
710 exclude: UnicodeCategorySet,
711 expected: UnicodeCategorySet,
712 ) {
713 assert_eq!(merge(include, exclude), expected);
714 }
715
716 #[test]
717 fn test_as_general_categories() {
718 let v = UnicodeVersion::V15_0_0;
719 let n = as_general_categories(["N"], v).expect("valid");
720 assert_eq!(n.len(), 3);
721 assert!(n.contains(&Nd) && n.contains(&Nl) && n.contains(&No));
722 let got = as_general_categories(["Lu", "Ll"], v).expect("valid");
723 assert_eq!(got.len(), 2);
724 assert!(got.contains(&Lu) && got.contains(&Ll));
725 assert!(as_general_categories(["Xx"], v).is_err());
726 }
727}