我该如何在 Rust 中对字符串进行大小写折叠操作?

9
我正在编写一个简单的全文搜索库,需要进行大小写折叠以检查两个单词是否相等。对于这种用例,现有的.to_lowercase().to_uppercase()方法是不够用
从crates.io的快速搜索中,我可以找到用于规范化和分词的库,但没有大小写折叠。regex-syntax确实有大小写折叠代码,但它没有在API中公开。

这些方法到底有什么不足之处?如果不知道你要解决的问题,很难回答你的问题。还有一些在char上定义的方法:https://doc.rust-lang.org/std/primitive.char.html#method.to_lowercase - BurntSushi5
1
@BurntSushi5 我已经在问题中添加了一些上下文,希望能有所帮助。 - Lambda Fairy
3
你最好的选择可能是 https://docs.rs/caseless/0.1.1/caseless/。 - BurntSushi5
4个回答

5
截至今天(2023年),caseless crate看起来没有维护,而ICU4X项目似乎是正确的选择。要应用大小写折叠,请参考icu_casemapping crate。要根据语言相关约定比较字符串,请参考icu_collator crate。关于如何在Rust中正确排序单词的良好介绍,请参见这里
有关Unicode理论和算法的文档,请参阅Unicode标准。 特别是:

有关 ICU4X 项目的文档,请参见此处

要使用 ICU4X,您可以将主包icu添加到Cargo.toml中,并访问单个模块(例如icu::collatoricu::datetime等),或者仅添加您实际需要的单个包(例如icu_collatoricu_datetime等)。

要检查两个单词是否相等,而不考虑大小写,您可以对字符串应用完全折叠大小写,然后检查二进制相等性。为此,您需要使用icu_casemapping::full_fold方法和数据提供程序,例如icu_testdata::unstable。 请注意,目前icu_casemapping的数据隐藏在功能icu_testdata/icu_casemapping后面,因此您需要在Cargo.toml文件中显式导入它:

[dependencies]
icu_casemapping = "0.7.1"
icu_testdata = { version = "1.1.2", features = ["icu_casemapping"] }

在未来的功能中,icu_testdata/icu_casemapping 可能会被添加到 icu_testdata 的默认功能中,因为 icu_casemapping 已经稳定下来。
以下是一个使用 icu_casemapping::full_fold 方法的简单示例:
use icu_casemapping::CaseMapping;

fn main() {
    let str1 = "Hello";
    let str2 = "hello";
    assert_ne!(str1, str2);
    let case_mapping = CaseMapping::try_new(&icu_testdata::unstable()).unwrap();
    assert_eq!(case_mapping.full_fold(str1), case_mapping.full_fold(str2));
}

请注意,目前的 icu_casemapping 包不包括规范化,这可能会在未来添加,详情请参见 此处 的讨论。
否则,要根据语言相关约定比较字符串,您可以使用 icu_collator 包,它允许自定义多个选项,如强度和区域设置。您可以在 此处 找到几个示例。

谢谢@lucatrv。如果您在答案中添加一些代码示例,我将很乐意接受它。 - Lambda Fairy

3
对于我的使用情况,我发现caseless crate最有用。
据我所知,这是唯一支持规范化的库。当您想要使“㎒”(U+3392 SQUARE MHZ)和“mhz”匹配时,这很重要。有关此操作方式的详细信息,请参见Unicode标准中的第3章-默认无大小写匹配
下面是一些示例代码,可以进行不区分大小写的字符串匹配:
extern crate caseless;
use caseless::Caseless;

let a = "100 ㎒";
let b = "100 mhz";

// These strings don't match with just case folding,
// but do match after compatibility (NFKD) normalization
assert!(!caseless::default_caseless_match_str(a, b));
assert!(caseless::compatibility_caseless_match_str(a, b));

要直接获取折叠后的字符串,您可以使用 default_case_fold_str 函数:
let s = "Twilight Sparkle ちゃん";
assert_eq!(caseless::default_case_fold_str(s), "twilight sparkle ちゃん");

Caseless没有暴露出一个对应的函数来进行规范化,但是你可以使用unicode-normalization库编写一个函数来实现:

extern crate unicode_normalization;
use caseless::Caseless;
use unicode_normalization::UnicodeNormalization;

fn compatibility_case_fold(s: &str) -> String {
    s.nfd().default_case_fold().nfkd().default_case_fold().nfkd().collect()
}

let a = "100 ㎒";
assert_eq!(compatibility_case_fold(a), "100 mhz");

请注意,需要多轮规范化和大小写转换才能得到正确的结果。
(感谢BurntSushi5指引我使用这个库。)

2
这个答案已经五年了。你现在会做得不同吗? - ccleve
@ccleve 请查看这个答案:https://dev59.com/b5vga4cB1Zd3GeqP9vJY#75526819 - Tanveer Badar

2

unicase创建物并没有直接暴露大小写折叠功能,但它提供了一种通用的包装类型,以不区分大小写的方式实现EqOrdHash。主分支(未发布)支持ASCII大小写折叠(作为优化),以及Unicode大小写折叠(尽管仅支持不变大小写折叠)。


2
如果有人想坚持使用标准库,我希望能提供一些实际的数据。我提取了完整列表,其中包含无法使用to_lowercaseto_uppercase转换的双字节字符。然后我运行了以下测试:
fn lowercase(left: char, right: char) -> bool {
   for c in left.to_lowercase() {
      for d in right.to_lowercase() {
         if c == d { return true }
      }
   }
   false
}

fn uppercase(left: char, right: char) -> bool {
   for c in left.to_uppercase() {
      for d in right.to_uppercase() {
         if c == d { return true }
      }
   }
   false
}

fn main() {
   let pairs = &[
      &['\u{00E5}','\u{212B}'],&['\u{00C5}','\u{212B}'],&['\u{0399}','\u{1FBE}'],
      &['\u{03B9}','\u{1FBE}'],&['\u{03B2}','\u{03D0}'],&['\u{03B5}','\u{03F5}'],
      &['\u{03B8}','\u{03D1}'],&['\u{03B8}','\u{03F4}'],&['\u{03D1}','\u{03F4}'],
      &['\u{03B9}','\u{1FBE}'],&['\u{0345}','\u{03B9}'],&['\u{0345}','\u{1FBE}'],
      &['\u{03BA}','\u{03F0}'],&['\u{00B5}','\u{03BC}'],&['\u{03C0}','\u{03D6}'],
      &['\u{03C1}','\u{03F1}'],&['\u{03C2}','\u{03C3}'],&['\u{03C6}','\u{03D5}'],
      &['\u{03C9}','\u{2126}'],&['\u{0392}','\u{03D0}'],&['\u{0395}','\u{03F5}'],
      &['\u{03D1}','\u{03F4}'],&['\u{0398}','\u{03D1}'],&['\u{0398}','\u{03F4}'],
      &['\u{0345}','\u{1FBE}'],&['\u{0345}','\u{0399}'],&['\u{0399}','\u{1FBE}'],
      &['\u{039A}','\u{03F0}'],&['\u{00B5}','\u{039C}'],&['\u{03A0}','\u{03D6}'],
      &['\u{03A1}','\u{03F1}'],&['\u{03A3}','\u{03C2}'],&['\u{03A6}','\u{03D5}'],
      &['\u{03A9}','\u{2126}'],&['\u{0398}','\u{03F4}'],&['\u{03B8}','\u{03F4}'],
      &['\u{03B8}','\u{03D1}'],&['\u{0398}','\u{03D1}'],&['\u{0432}','\u{1C80}'],
      &['\u{0434}','\u{1C81}'],&['\u{043E}','\u{1C82}'],&['\u{0441}','\u{1C83}'],
      &['\u{0442}','\u{1C84}'],&['\u{0442}','\u{1C85}'],&['\u{1C84}','\u{1C85}'],
      &['\u{044A}','\u{1C86}'],&['\u{0412}','\u{1C80}'],&['\u{0414}','\u{1C81}'],
      &['\u{041E}','\u{1C82}'],&['\u{0421}','\u{1C83}'],&['\u{1C84}','\u{1C85}'],
      &['\u{0422}','\u{1C84}'],&['\u{0422}','\u{1C85}'],&['\u{042A}','\u{1C86}'],
      &['\u{0463}','\u{1C87}'],&['\u{0462}','\u{1C87}']
   ];

   let (mut upper, mut lower) = (0, 0);

   for pair in pairs.iter() {
      print!("U+{:04X} ", pair[0] as u32);
      print!("U+{:04X} pass: ", pair[1] as u32);
      if uppercase(pair[0], pair[1]) {
         print!("to_uppercase ");
         upper += 1;
      } else {
         print!("             ");
      }
      if lowercase(pair[0], pair[1]) {
         print!("to_lowercase");
         lower += 1;
      }
      println!();
   }

   println!("upper pass: {}, lower pass: {}", upper, lower);
}

以下是结果。有趣的是,其中一对都失败了。但根据这个结果,使用to_uppercase是最好的选择
U+00E5 U+212B pass:              to_lowercase
U+00C5 U+212B pass:              to_lowercase
U+0399 U+1FBE pass: to_uppercase
U+03B9 U+1FBE pass: to_uppercase
U+03B2 U+03D0 pass: to_uppercase
U+03B5 U+03F5 pass: to_uppercase
U+03B8 U+03D1 pass: to_uppercase
U+03B8 U+03F4 pass:              to_lowercase
U+03D1 U+03F4 pass:
U+03B9 U+1FBE pass: to_uppercase
U+0345 U+03B9 pass: to_uppercase
U+0345 U+1FBE pass: to_uppercase
U+03BA U+03F0 pass: to_uppercase
U+00B5 U+03BC pass: to_uppercase
U+03C0 U+03D6 pass: to_uppercase
U+03C1 U+03F1 pass: to_uppercase
U+03C2 U+03C3 pass: to_uppercase
U+03C6 U+03D5 pass: to_uppercase
U+03C9 U+2126 pass:              to_lowercase
U+0392 U+03D0 pass: to_uppercase
U+0395 U+03F5 pass: to_uppercase
U+03D1 U+03F4 pass:
U+0398 U+03D1 pass: to_uppercase
U+0398 U+03F4 pass:              to_lowercase
U+0345 U+1FBE pass: to_uppercase
U+0345 U+0399 pass: to_uppercase
U+0399 U+1FBE pass: to_uppercase
U+039A U+03F0 pass: to_uppercase
U+00B5 U+039C pass: to_uppercase
U+03A0 U+03D6 pass: to_uppercase
U+03A1 U+03F1 pass: to_uppercase
U+03A3 U+03C2 pass: to_uppercase
U+03A6 U+03D5 pass: to_uppercase
U+03A9 U+2126 pass:              to_lowercase
U+0398 U+03F4 pass:              to_lowercase
U+03B8 U+03F4 pass:              to_lowercase
U+03B8 U+03D1 pass: to_uppercase
U+0398 U+03D1 pass: to_uppercase
U+0432 U+1C80 pass: to_uppercase
U+0434 U+1C81 pass: to_uppercase
U+043E U+1C82 pass: to_uppercase
U+0441 U+1C83 pass: to_uppercase
U+0442 U+1C84 pass: to_uppercase
U+0442 U+1C85 pass: to_uppercase
U+1C84 U+1C85 pass: to_uppercase
U+044A U+1C86 pass: to_uppercase
U+0412 U+1C80 pass: to_uppercase
U+0414 U+1C81 pass: to_uppercase
U+041E U+1C82 pass: to_uppercase
U+0421 U+1C83 pass: to_uppercase
U+1C84 U+1C85 pass: to_uppercase
U+0422 U+1C84 pass: to_uppercase
U+0422 U+1C85 pass: to_uppercase
U+042A U+1C86 pass: to_uppercase
U+0463 U+1C87 pass: to_uppercase
U+0462 U+1C87 pass: to_uppercase
upper pass: 46, lower pass: 8

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接