1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
#[cfg(feature = "serialization")]
use serde::{Deserialize, Serialize};
use smallvec::SmallVec;
#[cfg_attr(feature = "serialization", derive(Serialize, Deserialize))]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SplitOptions {
pub use_zh: bool,
pub use_en: bool,
pub bracket_as_entity: bool,
pub zh_quote_as_entity: bool,
pub en_quote_as_entity: bool,
}
impl Default for SplitOptions {
fn default() -> Self {
Self {
use_zh: true,
use_en: true,
bracket_as_entity: true,
zh_quote_as_entity: true,
en_quote_as_entity: true,
}
}
}
pub fn stn_split(text: &str) -> Vec<&str> {
let option = SplitOptions {
use_zh: true,
use_en: true,
bracket_as_entity: true,
zh_quote_as_entity: true,
en_quote_as_entity: true,
};
stn_split_with_options(text, &option)
}
pub fn stn_split_with_options<'a, 'b>(text: &'a str, options: &'b SplitOptions) -> Vec<&'a str> {
let mut res = vec![];
let mut char_indices = text.char_indices().peekable();
let mut quotes = SmallVec::<[char; 4]>::new();
let mut next_start = 0usize;
let mut as_normal = true;
let mut post_quotes = false;
let mut end_flag = false;
let mut last_char = ' ';
loop {
if let Some((idx, ch)) = char_indices.next() {
let next_char = char_indices.peek().map(|&(_, ch)| ch).unwrap_or('\n');
match (last_char, next_char) {
('“', '”') | ('‘', '’') | ('『', '』') | ('﹃', '﹄') => {
if options.zh_quote_as_entity {
continue;
}
}
('"', '"') | ('\'', '\'') => {
// quotes[..end] = [..lash_char];
if options.en_quote_as_entity && quotes.last() == Some(&next_char) {
continue;
}
}
_ => {} // do nothing
}
match ch {
'\r' | '\n' => {
// skip empty sentence
if idx > next_start {
res.push(&text[next_start..idx]); // skip the '\n'
}
next_start = idx + 1;
// 换行则重置
end_flag = false;
quotes.clear();
// 不执行
as_normal = false;
}
'?' | '!' => {
// 英文符号
if options.use_en {
end_flag = true;
as_normal = false;
}
}
'"' | '\'' => {
// 英文引号
if options.en_quote_as_entity {
if quotes.is_empty() {
quotes.push(ch);
} else if quotes.last() == Some(&ch) {
quotes.pop();
} else {
quotes.push(ch);
}
as_normal = false;
}
}
'.' => {
if options.use_en {
if last_char.is_ascii_digit() && next_char.is_ascii_digit() {
// 小数点
end_flag = false;
} else {
// 英文句号
end_flag = true;
}
as_normal = false;
}
}
'!' | '?' | '。' => {
// 中文标点
if options.use_zh {
end_flag = true;
as_normal = false;
}
}
'…' | '⋯' | '᠁' => {
// 省略号不能作为开头
// 对于省略号啥也不干
if options.use_zh || options.use_en {
end_flag = true;
as_normal = false;
}
}
// 判断引号结束是不是应该成句,看引号中是否为完整句子。
'“' | '‘' | '『' | '﹃' => {
if options.zh_quote_as_entity {
post_quotes = true;
}
}
'”' | '’' | '』' | '﹄' => {
if options.zh_quote_as_entity {
if let Some(quote) = quotes.pop() {
match (quote, ch) {
('“', '”') | ('‘', '’') | ('『', '』') | ('﹃', '﹄') =>
{
// do nothing
}
_ => {
// todo: error: maybe need todo something ?
// pop until find the quote pair
}
}
}
as_normal = false;
}
}
'[' | '(' | '{' | '⟨' | '(' | '〔' | '〈' | '《' | '【' => {
if options.bracket_as_entity {
// 左括号/左书名号 brackets
post_quotes = true;
}
}
']' | ')' | '}' | '⟩' | ')' | '〕' | '〉' | '》' | '】' => {
// 右括号/右书名号
if options.bracket_as_entity {
if let Some(quote) = quotes.pop() {
match (quote, ch) {
('[', ']')
| ('(', ')')
| ('{', '}')
| ('⟨', '⟩')
| ('(', ')')
| ('〔', '〕')
| ('〈', '〉')
| ('《', '》')
| ('【', '】') => {
// do nothing
}
_ => {
// todo: error: maybe need todo something ?
// pop until find the brackets pair
}
}
}
as_normal = false;
}
}
_ => {}
}
if as_normal {
if end_flag && quotes.is_empty() {
if idx > next_start {
res.push(&text[next_start..idx]);
}
next_start = idx;
}
// 比如 “全是东洋货!妈呀,”林小姐哭丧着脸说,“明儿叫我穿什么衣服?”
// 引号之前的前半句不完整,不应当进行切分,但引号中间的其他句子可能已经修改了成句标记。
end_flag = false;
} else {
as_normal = true;
}
if post_quotes {
quotes.push(ch);
post_quotes = false;
}
last_char = ch;
} else {
if next_start < text.len() {
res.push(&text[next_start..]);
}
break;
}
}
res
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_empty() {
assert!(stn_split("").is_empty());
assert!(stn_split("\n").is_empty());
assert!(stn_split("\n\n\n").is_empty());
}
#[test]
fn test_common() {
// common sentence
assert_eq!(
stn_split(concat!(
"中国是世界上历史最悠久的国家之一。",
"今天你怎么没给我打电话?",
"还是历来惯了,不以为非呢?还是丧了良心,明知故犯呢?",
"全国各民族大团结万岁!",
"宁为玉碎,不为瓦全。她要揭露!要控诉!!要以死作最后的抗争!!!",
"“什么?”男人强烈抗议道,“你以为我会随便退出娱乐圈吗?!”",
"周朴园:鲁大海,你现在没有资格和我说话——矿上已经把你开除了。",
"鲁大海:开除了?!",
"要普及现代信息技术教育,“计算机要从娃娃抓起”。",
"“坤包、坤表、坤车”里的“坤”,意思是女式的,女用的。",
"《毛泽东选集》对“李林甫”是这样注释的:“李林甫,公元八世纪人,唐玄宗时的一个宰相。《资治通鉴》说:‘李林甫为相,凡才望功业出己右及为上所厚、势位将逼己者,必百计去之;尤忌文学之士,或阳与之善,啗以甘言而阴陷之。世谓李林甫“口有蜜,腹有剑”。’”",
"大革命虽然失败了,但火种犹存。共产党人“从地下爬起来,揩干净身上的血迹,掩埋好同伴的尸首,他们又继续战斗了”。",
)),
vec![
"中国是世界上历史最悠久的国家之一。",
"今天你怎么没给我打电话?",
"还是历来惯了,不以为非呢?", "还是丧了良心,明知故犯呢?", // split it
"全国各民族大团结万岁!",
"宁为玉碎,不为瓦全。", "她要揭露!", "要控诉!!", "要以死作最后的抗争!!!", // split it
"“什么?”", "男人强烈抗议道,“你以为我会随便退出娱乐圈吗?!”", // split it
"周朴园:鲁大海,你现在没有资格和我说话——矿上已经把你开除了。",
"鲁大海:开除了?!",
"要普及现代信息技术教育,“计算机要从娃娃抓起”。",
"“坤包、坤表、坤车”里的“坤”,意思是女式的,女用的。",
"《毛泽东选集》对“李林甫”是这样注释的:“李林甫,公元八世纪人,唐玄宗时的一个宰相。《资治通鉴》说:‘李林甫为相,凡才望功业出己右及为上所厚、势位将逼己者,必百计去之;尤忌文学之士,或阳与之善,啗以甘言而阴陷之。世谓李林甫“口有蜜,腹有剑”。’”",
"大革命虽然失败了,但火种犹存。", "共产党人“从地下爬起来,揩干净身上的血迹,掩埋好同伴的尸首,他们又继续战斗了”。", // split it
]
);
}
#[test]
fn test_quoted() {
// quoted sentence
assert_eq!(
stn_split(concat!(
"丫姑折断几枝扔下来,边叫我的小名儿边说:“先喂饱你!”",
"“哎呀,真是美极了,”皇帝说,“我十分满意!”",
"从山脚向上望,只见火把排成许多“之”字形,一直连到天上。",
"“怕什么!海的美就在这里!”我说。",
"适当地改善自己的生活,岂但“你管得着吗”,而且是顺乎天理,合乎人情的。",
"现代画家徐悲鸿笔下的马,正如有的评论家所说的那样,“形神兼备,充满生机”。",
"唐朝的张嘉贞说它“制造奇特,人不知其所为”。",
"我听见韩麦尔先生对我说:“唉,总要把学习拖到明天,这正是阿尔萨斯人最大的不幸。现在那些家伙就有理由对我们说了:‘怎么?你们还自己说是法国人呢,你们连自己的语言都不会说,不会写!……’不过,可怜的小弗郎士,也并不是你一个人的过错。”",
"说他脸上有些妖气,一定遇见“美女蛇”了。",
"他们(指友邦人士)维持他们“秩序”监狱,就撕掉了他们的“文明”的面具。",
)),
vec![
"丫姑折断几枝扔下来,边叫我的小名儿边说:“先喂饱你!”",
"“哎呀,真是美极了,”皇帝说,“我十分满意!”",
"从山脚向上望,只见火把排成许多“之”字形,一直连到天上。",
"“怕什么!海的美就在这里!”", "我说。", // todo: maybe not split it
"适当地改善自己的生活,岂但“你管得着吗”,而且是顺乎天理,合乎人情的。",
"现代画家徐悲鸿笔下的马,正如有的评论家所说的那样,“形神兼备,充满生机”。",
"唐朝的张嘉贞说它“制造奇特,人不知其所为”。",
"我听见韩麦尔先生对我说:“唉,总要把学习拖到明天,这正是阿尔萨斯人最大的不幸。现在那些家伙就有理由对我们说了:‘怎么?你们还自己说是法国人呢,你们连自己的语言都不会说,不会写!……’不过,可怜的小弗郎士,也并不是你一个人的过错。”",
"说他脸上有些妖气,一定遇见“美女蛇”了。",
"他们(指友邦人士)维持他们“秩序”监狱,就撕掉了他们的“文明”的面具。",
]
);
}
#[test]
fn test_dotted() {
// dotted sentence
assert_eq!(
stn_split(concat!(
"那孩子含着泪唱着:“……世上只有妈妈好,没妈的孩子像根草……”",
"各种鲜花争奇斗艳:菊花、玫瑰、马蹄莲、郁金香……",
"他吃力地张开嘴:“你……要……坚持……下……去……”",
)),
vec![
"那孩子含着泪唱着:“……世上只有妈妈好,没妈的孩子像根草……”",
"各种鲜花争奇斗艳:菊花、玫瑰、马蹄莲、郁金香……",
"他吃力地张开嘴:“你……要……坚持……下……去……”",
]
);
}
#[test]
fn test_special_quote() {
// special quote sentence
assert_eq!(
stn_split(concat!(
"林小姐哭丧着脸说:“妈呀,全是东洋货!明儿叫我穿什么衣服?”",
"“妈呀,全是东洋货!明儿叫我穿什么衣服?”林小姐哭丧着脸说。",
"“妈呀,”林小姐哭丧着脸说,“全是东洋货!明儿叫我穿什么衣服?”",
"“全是东洋货!妈呀,”林小姐哭丧着脸说,“明儿叫我穿什么衣服?”",
)),
vec![
"林小姐哭丧着脸说:“妈呀,全是东洋货!明儿叫我穿什么衣服?”",
"“妈呀,全是东洋货!明儿叫我穿什么衣服?”",
"林小姐哭丧着脸说。", // todo: maybe not split it
"“妈呀,”林小姐哭丧着脸说,“全是东洋货!明儿叫我穿什么衣服?”",
"“全是东洋货!妈呀,”林小姐哭丧着脸说,“明儿叫我穿什么衣服?”",
]
);
}
#[test]
fn test_mix() {
// chinese & english & point number
assert_eq!(
stn_split(concat!(
"中文和外文同时大量混排(如讲解英语语法的中文书),为避免中文小圆圈的句号“。”和西文小圆点儿的句号“.”穿插使用的不便,可以统统采用西文句号“.”。",
"这个句子应当翻译成He loves sports.",
"焦耳定律的公式是:Q = I2RT.",
"计算所得的结果是48.2%.",
"“‘”应该和“’”成对使用。",
)),
vec![
"中文和外文同时大量混排(如讲解英语语法的中文书),为避免中文小圆圈的句号“。”和西文小圆点儿的句号“.”穿插使用的不便,可以统统采用西文句号“.”。",
"这个句子应当翻译成He loves sports.",
"焦耳定律的公式是:Q = I2RT.",
"计算所得的结果是48.2%.",
"“‘”应该和“’”成对使用。",
]
);
}
}