最清晰的脚印是留在最泥泞的路上的
大家好,我是柒八九。
前言
提出一个小小的问题。大家按照自己的开发语言的特性,想想结果是啥?
"🤦🏼♂️"这个Emoji
的长度是多少?
如果,现在你用电脑阅读本文,你可以轻松的打开xx PlayGround
(xx
可以为Js/Java/Rust等
)。然后会得到属于自己语言的结果。
如果,你现在手头没电脑,无法亲自验证,我来直接告诉你答案。上述Emoji
在每种语言环境下的结果都不统一。(当然,有些语言内核使用的机制一样,结果可能也一样)。
也就是说,在编程层面,这不是一种 所见即所得的表现形式。大家这里可能会纳闷了,我要知道这个有啥?现在举一个例子,在前端页面中,我们总是会有统计用户字数的输入框,但是由于用户输入了Emoji
,从用户的角度来看,这就是一个字符,但是在编程层面,如果不做一次解析的话,我们会得到千奇百怪的答案。
然后,我们再来一个让人匪夷所思的例子。在浏览器中,尝试复制如下代码,然后进行观察答案。结果是不是又再一次颠覆你的所学。
"Å" === "Å";
平时,我们时不时的会提到UTF-8/UTF-16/UTF-32
它们到底是个啥?又有啥关系和区别呢?
还有其他的例子就不一一列举了。之所以会出现这么多让人匪夷所思的结果。一切的根源都是Unicode
的闹的。
所以,今天我们就来谈谈这是何方神圣。
在2000
多年前,我们那迷人的老祖宗,秦始皇,就实现了车同轨,书同文,划破地域障碍,从而给不同地方的人在交流上开辟了新的空间。虽然,有些地方还存在十里不同音,百里不通俗的情况(我老家山西就是这种情况)。但是,在官方层面或者书面层面上,大家可以沟通无阻。
好了,天不早了,干点正事哇。
我们能所学到的知识点
- 前置知识点
Unicode
是个啥?UTF-8
又是什么?UTF-32
问题Unicode
病症- 如何检测
扩展形素簇
- "Å" !== "Å" !== "Å"
- Unicode 取决于区域设置
1. 前置知识点
前置知识点,只是做一个概念的介绍,不会做深度解释。因为,这些概念在下面文章中会有出现,为了让行文更加的顺畅,所以将本该在文内的概念解释放到前面来。如果大家对这些概念熟悉,可以直接忽略
同时,由于阅读我文章的群体有很多,所以有些知识点可能我视之若珍宝,尔视只如草芥,弃之如敝履。以下知识点,请酌情使用。
ASCll
ASCII(American Standard Code for Information Interchange
)的缩写,发音为ask-key
。ASCII
是一种用于表示字符的7位标准编码
,其中包括字母、数字和标点符号。
7 位编码允许计算机编码总共128个字符
,包括数字 0-9、大写和小写字母 A-Z 以及一些标点符号。然而,这 128 位编码仅适用于英语用户。
ASCII 的功能
ASCII
的建立旨在实现各种数据处理设备之间的兼容性,从而使这些组件能够成功地相互通信。ASCII
使制造商能够生产可以确保在计算机中正确运行的组件。ASCII
使人机互动。
ASCII 在计算机系统中的工作原理
当我们按下键盘上的键,例如字母D
时,电子信号被发送到计算机的CPU
进行处理和存储在内存中。每个字符都被转换为其对应的二进制形式。计算机将字母处理为一个字节,实际上是一系列电子状态的开和关。当计算机完成处理字节后,系统中安装的软件将字节转换回,并在屏幕上显示。字母 D 被转换为01000100
。
TextEncoder 和 TextDecoder
TextEncoder
和 TextDecoder
是 JavaScript
中用于处理字符编码的内置对象。它们通常用于在不同字符编码之间进行文本的编码和解码。
TextEncoder
TextEncoder
是用于将字符串文本编码为字节数组(通常是UTF-8
编码)的对象。- 它提供了一个
encode()
方法,接受一个字符串作为参数,并返回一个包含字节的Uint8Array
对象。 TextEncoder
用于将文本数据转换为字节数据,以便在网络传输、文件读写或其他需要字节数据的情况下使用。
示例:
const encoder = new TextEncoder();
const text = "前端柒八九!";
const bytes = encoder.encode(text); // 将文本编码为字节数组
TextDecoder
TextDecoder
是用于将字节数组解码为字符串文本的对象。- 它提供了一个
decode()
方法,接受一个包含字节的Uint8Array
对象,并返回相应的字符串。 TextDecoder
用于将字节数据还原为文本,通常用于处理来自网络请求或文件的字节数据。
示例:
const decoder = new TextDecoder("UTF-8");
const bytes = new Uint8Array([
72, 101, 108, 108, 111, 44, 32, 87, 111, 114, 108, 100, 33,
]);
const text = decoder.decode(bytes); // 将字节数组解码为字符串
这些对象在处理多语言文本、字符编码转换和处理国际化内容时非常有用,使 JavaScript
能够处理不同字符编码之间的数据转换。
Emoji
Emoji
是可以插入文字的图形符号。
它是一个日语词,e
表示"絵",moji
表示"文字"。连在一起,就是"絵文字"。
2010 年,Unicode
开始为 Emoji
分配码点
。也就是说,现在的 Emoji 符号就是一个文字,它会被渲染为图形。
想了解更多,可以翻阅Emoji 简介
2. Unicode 是个啥?
Unicode
是一个旨在统一所有人类语言(包括过去和现在的语言)并使它们与计算机兼容的标准。
Unicode
是一个将不同字符分配给唯一编号的表格。
例如:
- 拉丁字母
A
被分配编号65
。 - 阿拉伯字母 Seen
س
是1587
。 - 片假名字母 Tu
ツ
是12484
- 音乐符号 G 调号
𝄞
是119070
。 💩
是128169
。
Unicode
将这些编号称为码位(code points
)。
由于这套准则是全球都认准的,所以我们采用这套规则,就可以达到书同文的情况,来自不同语言环境下的人,可以阅读彼此的文本。
有如下的关系链子。 一个Unicode
对应着一个字符,并且该字符拥有几乎唯一的码位
。
Unicode
===字符
⟷码位
.
Unicode 有多大?
目前,最大的已定义码位是0x10FFFF
。(0x10FFFF
是一个十六进制数,将其转换为十进制,其值为 1,114,111
。)这给我们提供了大约 110 万个码位的空间。
目前已定义了约 15%
(约 170,000
个),另外 11%
(为私人使用)已被保留。其余约 800,000
个码位目前尚未分配,它们可能在未来成为字符。
大致如下图所示:
大正方形
包含 65,536 个字符。小正方形
包含 256 个字符。- 整个
ASCII
字符集仅占位于左上角的小红色正方形的一半。
私人使用区(Private Use)
私人使用区
是为应用程序开发人员保留的码位
,不会由 Unicode
本身定义。
例如,Unicode
中没有为苹果标志保留位置,因此苹果将它放在了 U+F8FF
,这位于私人使用区
。在任何其他字体中,它将呈现为缺失的字符
,但在与 macOS
一起提供的字体中,我们将看到苹果图标
。
。
私人使用区主要用于图标字体:
U+1F4A9 是什么意思?
这是一种写码位值的约定
。前缀 U+
表示 Unicode
,而 1F4A9
是一个十六进制的码位编号。
U+1F4A9
具体表示的是 💩
。(是不是我们多了一种很委婉的"表扬别人"方式)
3. UTF-8 又是什么?
UTF-8
是一种编码方式。
编码是我们将码位
存储在内存中的方法。在互联网和许多操作系统中,UTF-8
是默认的文本编码。
最简单的 Unicode
编码是 UTF-32
。它将码位简单地存储为 32 位整数。因此,U+1F4A9
变成了 00 01 F4 A9
,占用了四个字节。UTF-32
中的任何其他码位也将占用四个字节。由于最高定义的码位是 U+10FFFF
,因此任何码位都能够容纳。
UTF-8
通常用于存储和传输文本
UTF-16
用于某些操作系统和编程语言
UTF-16
被许多系统采用。其中包括Microsoft Windows
、Objective-C
、Java
、JavaScript
、.NET
、Python 2
等UTF-32
适用于需要直接操作Unicode
代码点的情况
UTF-8 有多少字节?
UTF-8
是一种可变长度的编码方式。
一个码位可能被编码为一个到四个字节的序列。
以下是 UTF-8
编码的表示形式,根据不同的码位范围使用不同数量的字节
码位范围 | Byte 1 | Byte 2 | Byte 3 | Byte 4 |
---|---|---|---|---|
U+0000..007F | 0xxxxxxx | |||
U+0080..07FF | 110xxxxx | 10xxxxxx | ||
U+0800..FFFF | 1110xxxx | 10xxxxxx | 10xxxxxx | |
U+10000..10FFFF | 11110xxx | 10xxxxxx | 10xxxxxx | 10xxxxxx |
这些规则描述了如何将不同码位范围内的 Unicode
字符编码为 UTF-8 字节序列
。
如果将这些内容与 Unicode
表结合起来,我们将看到
英语
使用1
个字节进行编码,西里尔字母
、拉丁欧洲语言
、希伯来语
和阿拉伯语
需要2
个字节,中文
、日语
、韩语
、其他亚洲语言和表情符号需要3
或4
个字节。
以下是一些重要的要点:
首先,
UTF-8
与ASCII
是字节兼容的。码位0..127
,即旧的ASCII
字符,使用一个字节进行编码,而且它们的字节表示完全相同。例如,U+0041
(A
,拉丁大写字母A
)就是41
,一个字节。- 任何纯
ASCII
文本也是有效的UTF-8
文本,而且只使用码位0..127
的UTF-8
文本可以直接读取为 ASCII。
- 任何纯
其次,
UTF-8
对于基本拉丁字符
来说是空间高效的。- 对于像
HTML
标签或JSON
这样的技术字符串来说,这是有意义的。
- 对于像
第三,
UTF-8
内置了错误检测和恢复功能。- 第一个字节的
前缀
总是与第 2 到第 4 个字节不同。这样,我们始终可以确定是否正在查看完整和有效的UTF-8
字节序列,或者是否有遗漏。 - 然后,我们可以通过
向前
或向后
移动,直到找到正确序列的开头来进行纠正。
- 第一个字节的
还有一些重要的结论:
- 我们无法通过计算字节来确定字符串的长度。
- 我们无法随机跳到字符串的中间并开始阅读。
- 我们无法通过在任意字节偏移处进行切割来获取子字符串,可能会切断字符的一部分。
如果硬要这么做的话,系统会给你一个�
。
“�”是什么?
U+FFFD
,即替换字符(Replacement Character
),只是 Unicode
表中的另一个码位。应用程序和库可以在检测到 Unicode 错误时
使用它。
如果将码位的一半切掉,那么另一半也就没什么用了,除了显示错误。这时就会使用�
。
JS 版本
const text = "前端柒八九";
const encoder = new TextEncoder();
const bytes = encoder.encode(text);
const partial = bytes.slice(0, 11);
const decoder = new TextDecoder("UTF-8");
const result = decoder.decode(partial);
console.log(result); // 输出 "前端柒�"
Rust 版本
fn main() {
let text = "前端柒八九";
let bytes = text.as_bytes();
let partial = &bytes[0..11];
let result = String::from_utf8_lossy(partial);
println!("{}", result); // 输出 "前端柒�"
}
在 JavaScript
中使用 TextEncoder
和 TextDecoder
来处理编码,而在 Rust
中使用 String::from_utf8_lossy
来处理字节。它们的目标是在 UTF-8
编码中处理文本并截取部分字节。
4. UTF-32 问题
UTF-32
非常适用于处理码位。它的编码方式中,每个码位始终是 4 个字节,那么strlen(s) == sizeof(s) / 4
,substring(0, 3) == bytes[0, 12]
(上面代码为伪代码)等等。
问题在于,我们不想处理码位。一个码位即不是一个书写单位,又并不总是代表一个字符。我们应该处理的是扩展形素簇
(extended grapheme clusters
),或简称为形素
(graphemes
)。
形素
是在特定书写系统的上下文中的最小可区分的书写单位。
例如,ö
是一个形素,é
也是一个形素。还有像각
这样的形素。基本上,形素是用户认为是一个字符的单元。
问题是,在 Unicode
中,一些形素
是由多个码位编码的!
例如,é
(一个单一的形素)在 Unicode
中编码为 e
(U+0065
拉丁小写字母 E)+ ´
(U+0301
连接重音符)。两个码位!
它也可能不止两个:
☹️
是U+2639
+U+FE0F
👨🏭
是U+1F468
+U+200D
+U+1F3ED
🚵🏻♀️
是U+1F6B5
+U+1F3FB
+U+200D
+U+2640
+U+FE0F
y̖̠͍̘͇͗̏̽̎͞
是U+0079
+U+0316
+U+0320
+U+034D
+U+0318
+U+0347
+U+0357
+U+030F
+U+033D
+U+030E
+U+035E
即使在最宽的编码 UTF-32
中,👨🏭
仍需要三个 4 字节单元来进行编码。它仍然需要被视为一个单独的字符。
我们可以将 Unicode
本身(没有任何编码)视为可变长度的。
扩展形素簇(Extended Grapheme Cluster)是一个或多个 Unicode 码位的序列,必须将其视为一个单独的、不可分割的字符。
因此,在码位级别上:不能只取序列的一部分,它总是应该作为一个整体选择、复制、编辑或删除。
不正确使用形素簇
会导致像这样的错误:
无论是否选择UTF-32
还是UTF-8
在处理形素
上遇到相似的问题。所以如何使用形素
才是我们应该关心的。
5. Unicode 病症
上面的例子中大部分都是涉及到表情符号
,这会给人一种错觉。Unicode
只有在表示表情符号时,会遇到问题。--其实不是。
扩展形素簇
也用于常见的语言。
例如:
ö
(德语)是一个单一字符,但包含多个码位(U+006F
U+0308
)。ą́
(立陶宛语)是U+00E1
U+0328
。각
(韩语)是U+1100
U+1161
U+11A8
。
所以,问题不仅仅是表情符号。
"🤦🏼♂️".length 是多少?
不同的编程语言给出了不同的结果。
Python 3:
>>> len("🤦🏼♂️")
5
JavaScript / Java / C#:
>> "🤦🏼♂️".length
7
Rust:
println!("{}", "🤦🏼♂️".len());
// => 17
不同的语言使用不同的内部字符串表示(UTF-32
、UTF-16
、UTF-8
),并以存储字符的单位(整数、短整数、字节)来报告长度。
但是!如果你问任何不懂编程理论的人,他们会给你一个明确的答案:🤦🏼♂️
字符串的长度是 1。
这就是扩展形素簇
的意义:人们视为单一字符的内容。在这种情况下,🤦🏼♂️
显然是一个单一字符。
🤦🏼♂️
由 5 个码位
组成(U+1F926
U+1F3FB
U+200D
U+2642
U+FE0F
)仅仅是实现细节。它不应该被分开,不应该被计为多个字符,文本光标不应该定位在其中,不应该被部分选择,等等。
这是文本的一个不可分割的单位。在内部,它可以被编码为任何形式,但对于面向用户的 API,应该将其视为一个整体。
唯一正确处理此问题的现代语言是 Swift
:
print("🤦🏼♂️".count)
// => 1
而对于我们比较熟悉的JS
和Rust
,我们可以使用一些方式做一下封装。
function visibleLength(str) {
return [...new Intl.Segmenter().segment(str)].length;
}
visibleLength("🤦🏼♂️"); // 输出结果为1
当然,我们还可以校验其他的形素
。
visibleLength("ö"); // => 1
visibleLength("👩💻"); // => 1
visibleLength("👩💻👩❤️💋👩"); // => 2
visibleLength("と日本語の文章"); // => 7
但是呢,Intl.Segmenter
的兼容性不是很好。
如果,我们要实现多浏览器适配,我们可以找一些第三方的库。
如果想了解更多细节,可以参考JS 如何正确处理 Unicode
对于Rust
我们可以使用unicode_segmentationcrate。
extern crate unicode_segmentation; // "1.9.0"
use std::collections::HashSet;
use unicode_segmentation::UnicodeSegmentation;
fn count_unique_grapheme_clusters(s: &str) -> usize {
let is_extended = true;
s.graphemes(is_extended).collect::<HashSet<_>>().len()
}
fn main() {
assert_eq!(count_unique_grapheme_clusters(""), 0);
assert_eq!(count_unique_grapheme_clusters("🤦🏼♂️"), 1);
assert_eq!(count_unique_grapheme_clusters("🇺🇸"), 1);
}
6. 如何检测扩展形素簇
大多数编程语言选择了简单的方式,允许我们迭代字符串时使用 1-2-4 字节的块,但不支持直接处理扩展形素簇。
由于它是默认方式,结果我们看到了损坏的字符串:
如果遇到这种问题,我们首先的就是应该想到使用Unicode
库。
使用库
即使是像 strlen
、indexOf
或 substring
这样的基本操作也应该使用 Unicode
库!
例如:
C/C++/Java
:使用 ICU。这是Unicode
自身发布的库,包含了关于文本分割的所有规则。Swift
:只需使用标准库。Swift 默认情况下会正确处理。Javascript
的话,我们上面提到过,可以使用浏览器内置功能Intl.Segmenter
或者graphemer/text-segmentation
Rust
而言,我们可以使用unicode_segmentation
不管选择哪种方式,确保它使用的是新版本的 Unicode
,因为形素的定义会随版本而变化。
Unicode 规则更新
从大约 2014
年开始,Unicode
每年都会发布其标准的重大修订版本。
每年更新
随之而来的不良反映就是,定义形素簇
的规则每年也会发生变化。今天被认为是由两个或三个独立码位组成的序列,明天可能会成为一个形素簇!这种朝令夕改的做法,很是让人深恶痛绝。
更糟糕的是,我们自己的应用程序的不同版本可能运行在不同的 Unicode
标准上,并报告不同的字符串长度!
7. "Å" !== "Å" !== "Å"
将其中任何一个复制到你的 JavaScript 控制台:
"Å" === "Å";
"Å" === "Å";
"Å" === "Å";
你会得到让你匪夷所思的答案。没错,它们的打印结果都是false
。
还记得之前的,ö
是由两个码位组成,U+006F
U+0308
。基本上,Unicode
提供了多种编写字符如 ö
或 Å
的方式。
- 通过将普通的拉丁字母
A
与一个组合字符
组合成Å
, - 或者使用已经预先组合的码位
U+00C5
。
因为,它们看起来是相同的(Å
与 Å
),所以从用户的角度,我们就认为它们应该是相同的,但结果却和我们的想法大相径庭。
这就是为什么我们需要规范化。有四种形式:
这里先从NFD
和NFC
介绍。
NFD
(Normalization Form C
) 尝试将一切都分解为最小可能的部分,并如果存在多个部分,则按照规范顺序对这些部分进行排序。- 它消除任何规范化差异,并生成一个分解的结果
NFC
(Normalization Form C
),尝试将一切组合成已经预先组合的形式(如果存在)- 它消除任何规范化差异,通常生成一个合成的结果
不同的形式用于不同的用例,以确保文本在不同的方式下都保持一致。所以,尽管"Å" !== "Å" !== "Å"
,但通过适当的规范化,我们可以使它们等同。
对于某些字符,Unicode
中还存在多个版本。例如,有 U+00C5
带有上面环圈的拉丁大写字母 A
,但还有外观相同的 U+212B
Ångström
符号。
这些字符在规范化过程中也会被替换,以确保它们的一致性。
NFD
和 NFC
被称为“规范化规范”(canonical normalization)。另外两种形式是“兼容规范化”(compatibility normalization):
NFKD
试图将所有内容分解,并使用默认形式替换视觉变体。- 它消除规范化和兼容性差异,并生成一个分解的结果
NFKC
试图将所有内容组合在一起,同时用默认形式替换视觉变体。- 它消除规范化和兼容性差异,并通常生成一个合成的结果
视觉变体
是表示相同字符的独立 Unicode
码位,但它们应该呈现不同的方式。比如,①
、⁹
或 𝕏
。
在比较字符串或搜索子字符串之前,进行规范化!
在JavaScript
中,我们可以使用 normalize()
方法来实现 NFC
(Normalization Form C)和 NFD
(Normalization Form D)。
const str1 = "Å";
const str2 = "Å";
const normalizedStr1 = str1.normalize("NFC"); // NFC 形式
const normalizedStr2 = str2.normalize("NFC"); // NFC 形式
console.log(normalizedStr1 === normalizedStr2); // true
上述代码首先使用 normalize('NFC')
方法将两个字符串都转换为 NFC 形式,然后比较它们是否相等。这将使 "Å" 和 "Å" 的比较结果为 true
。
如果使用 NFD
形式,只需将 normalize('NFC')
更改为 normalize('NFD')
即可。
8. Unicode 取决于区域设置
俄罗斯
名字尼古拉
在Unicode
中编码为 U+041D 0438 043A 043E 043B 0430 0439
。
保加利亚
名字尼古拉
也写成 U+041D 0438 043A 043E 043B 0430 0439
。
它们的Unicode
值完全一样,但是所显示的字体信息却不尽相同。是不是有种小脑萎缩的感觉。
然后心中有一个 🤔,计算机如何知道何时呈现保加利亚风格的字形,何时使用俄罗斯的字形?
其实,计算机也不知。Unicode
并不是一个完美的系统,它有很多不足之处。其中一个问题是将本应呈现不同外观的字形分配给相同的码位,比如西里尔字母的小写字母 K
和保加利亚的小写字母 K
(都是 U+043A
)。
针对一些表音语言
这块还能好点,但是到了我们大亚洲
,很多国家的文字都是表意的。许多汉字、日语和韩语表意字形的写法都截然不同,但被分配了相同的码位。
Unicode
的动机是为了节省码位空间。渲染信息应该在字符串外部以区域设置
/语言元数据
的方式传递。
在实践中,依赖于区域设置带来了许多问题:
- 作为元数据,
区域设置
通常会丢失。 - 人们不限于使用单一区域设置。例如,我们可以阅读和写作中文,美国英语、英国英语、德语和俄语。
- 难以混合和匹配。比如在保加利亚文本中使用俄罗斯名字,反之亦然。
- 没有地方可以指定区域设置。即使制作上面的两个屏幕截图也不容易,因为在大多数软件中,没有下拉菜单或文本输入来更改区域设置。
9. 处理特殊语言
另一个不幸的例子是土耳其语
中无点 i
的 Unicode
处理。
与英语不同,土耳其语
有两种 I 变体:有点
和无点
。
Unicode
决定重用ASCII
中的 I 和 i,并只添加了两个新的码位:İ
和ı
。
这导致了在相同输入上 toLowerCase
/toUpperCase
表现不同:
var en_US = Locale.of("en", "US");
var tr = Locale.of("tr");
System.out.println("I".toLowerCase(en_US)); // => "i"
System.out.println("I".toLowerCase(tr)); // => "ı"
System.out.println("i".toUpperCase(en_US)); // => "I"
System.out.println("i".toUpperCase(tr)); // => "İ"
所以,我们在不知道字符串是用哪种语言编写的情况下将字符串转换为小写,会出现问题。
如果我们项目中涉及到土耳其语
的字符转换,在 JS 中toLowerCase
是达不到上面的要求的。因为,在JavaScript
中,toLowerCase
方法默认使用Unicode
规范进行转换,根据Unicode
的规范,大写 I
被转换为小写 i
,而不是 ı
。这是因为JavaScript
的toLowerCase
方法按照Unicode
的标准工作。
要想使用JS
正确处理上面的问题,我们就需要额外的 API.
"I".toLocaleLowerCase("tr-TR"); // => "ı"
"i".toLocaleUpperCase("tr-TR"); // => "İ"
我们也可以通过对String.prototype
上做一层封装。
String.prototype.turkishToUpper = function () {
var string = this;
var letters = { i: "İ", ş: "Ş", ğ: "Ğ", ü: "Ü", ö: "Ö", ç: "Ç", ı: "I" };
string = string.replace(/(([iışğüçö]))+/g, function (letter) {
return letters[letter];
});
return string.toUpperCase();
};
String.prototype.turkishToLower = function () {
var string = this;
var letters = { İ: "i", I: "ı", Ş: "ş", Ğ: "ğ", Ü: "ü", Ö: "ö", Ç: "ç" };
string = string.replace(/(([İIŞĞÜÇÖ]))+/g, function (letter) {
return letters[letter];
});
return string.toLowerCase();
};
// 代码演示
"DİNÇ".turkishToLower(); // => dinç
"DINÇ".turkishToLower(); // => dınç
这样就可以正确规避JS
针对土耳其语言中的准换问题。
在Rust
中,我们可以使用如下代码:
fn turkish_to_upper(input: &str) -> String {
let letters = [
('i', "İ"),
('ş', "Ş"),
('ğ', "Ğ"),
('ü', "Ü"),
('ö', "Ö"),
('ç', "Ç"),
('ı', "I"),
];
let mut result = String::new();
for c in input.chars() {
let mut found = false;
for &(source, target) in &letters {
if c == source {
result.push_str(target);
found = true;
break;
}
}
if !found {
result.push(c);
}
}
result.to_uppercase()
}
fn turkish_to_lower(input: &str) -> String {
let letters = [
('İ', "i"),
('I', "ı"),
('Ş', "ş"),
('Ğ', "ğ"),
('Ü', "ü"),
('Ö', "ö"),
('Ç', "ç"),
];
let mut result = String::new();
for c in input.chars() {
let mut found = false;
for &(source, target) in &letters {
if c == source {
result.push_str(target);
found = true;
break;
}
}
if !found {
result.push(c);
}
}
result.to_lowercase()
}
fn main() {
let input = "İşğüöçı";
let upper_result = turkish_to_upper(input);
let lower_result = turkish_to_lower(input);
println!("Upper: {}", upper_result); //Upper: İŞĞÜÖÇI
println!("Lower: {}", lower_result); // Lower: işğüöçı
}
后记
分享是一种态度。
全文完,既然看到这里了,如果觉得不错,随手点个赞和“在看”吧。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。