1const ZERO: &[(u32, u32)] = &[
14 (0x0300, 0x036F), (0x0483, 0x0489),
16 (0x0591, 0x05BD),
17 (0x0610, 0x061A),
18 (0x064B, 0x065F),
19 (0x0670, 0x0670),
20 (0x06D6, 0x06DC),
21 (0x0E31, 0x0E31),
22 (0x0E34, 0x0E3A),
23 (0x0E47, 0x0E4E),
24 (0x1160, 0x11FF), (0x135D, 0x135F),
26 (0x1AB0, 0x1AFF),
27 (0x1DC0, 0x1DFF),
28 (0x200B, 0x200F), (0x2028, 0x202E),
30 (0x2060, 0x2064),
31 (0x20D0, 0x20F0),
32 (0xFE00, 0xFE0F), (0xFE20, 0xFE2F),
34 (0xFEFF, 0xFEFF),
35 (0x1D165, 0x1D169),
36 (0x1D16D, 0x1D172),
37 (0xE0100, 0xE01EF),
38];
39
40const WIDE: &[(u32, u32)] = &[
42 (0x1100, 0x115F), (0x231A, 0x231B),
44 (0x2329, 0x232A),
45 (0x23E9, 0x23EC),
46 (0x23F0, 0x23F0),
47 (0x23F3, 0x23F3),
48 (0x25FD, 0x25FE),
49 (0x2614, 0x2615),
50 (0x2648, 0x2653),
51 (0x267F, 0x267F),
52 (0x2693, 0x2693),
53 (0x26A1, 0x26A1),
54 (0x26AA, 0x26AB),
55 (0x26BD, 0x26BE),
56 (0x26C4, 0x26C5),
57 (0x26CE, 0x26CE),
58 (0x26D4, 0x26D4),
59 (0x26EA, 0x26EA),
60 (0x26F2, 0x26F3),
61 (0x26F5, 0x26F5),
62 (0x26FA, 0x26FA),
63 (0x26FD, 0x26FD),
64 (0x2705, 0x2705),
65 (0x270A, 0x270B),
66 (0x2728, 0x2728),
67 (0x274C, 0x274C),
68 (0x274E, 0x274E),
69 (0x2753, 0x2755),
70 (0x2757, 0x2757),
71 (0x2795, 0x2797),
72 (0x27B0, 0x27B0),
73 (0x27BF, 0x27BF),
74 (0x2B1B, 0x2B1C),
75 (0x2B50, 0x2B50),
76 (0x2B55, 0x2B55),
77 (0x2E80, 0x2E99),
78 (0x2E9B, 0x2EF3),
79 (0x2F00, 0x2FD5),
80 (0x2FF0, 0x2FFB),
81 (0x3000, 0x303E), (0x3041, 0x3096), (0x3099, 0x30FF), (0x3105, 0x312F),
85 (0x3131, 0x318E), (0x3190, 0x31E3),
87 (0x31F0, 0x321E),
88 (0x3220, 0x3247),
89 (0x3250, 0x4DBF),
90 (0x4E00, 0xA48C), (0xA490, 0xA4C6),
92 (0xA960, 0xA97C),
93 (0xAC00, 0xD7A3), (0xF900, 0xFAFF),
95 (0xFE10, 0xFE19),
96 (0xFE30, 0xFE52),
97 (0xFE54, 0xFE66),
98 (0xFE68, 0xFE6B),
99 (0xFF01, 0xFF60), (0xFFE0, 0xFFE6),
101 (0x16FE0, 0x16FE4),
102 (0x17000, 0x18CD5),
103 (0x1B000, 0x1B152),
104 (0x1B164, 0x1B167),
105 (0x1B170, 0x1B2FB),
106 (0x1F004, 0x1F004),
107 (0x1F0CF, 0x1F0CF),
108 (0x1F18E, 0x1F18E),
109 (0x1F191, 0x1F19A),
110 (0x1F200, 0x1F320),
111 (0x1F32D, 0x1F335),
112 (0x1F337, 0x1F37C),
113 (0x1F37E, 0x1F393),
114 (0x1F3A0, 0x1F3CA),
115 (0x1F3CF, 0x1F3D3),
116 (0x1F3E0, 0x1F3F0),
117 (0x1F3F4, 0x1F3F4),
118 (0x1F3F8, 0x1F43E),
119 (0x1F440, 0x1F440),
120 (0x1F442, 0x1F4FC),
121 (0x1F4FF, 0x1F53D),
122 (0x1F54B, 0x1F54E),
123 (0x1F550, 0x1F567),
124 (0x1F57A, 0x1F57A),
125 (0x1F595, 0x1F596),
126 (0x1F5A4, 0x1F5A4),
127 (0x1F5FB, 0x1F64F),
128 (0x1F680, 0x1F6C5),
129 (0x1F6CC, 0x1F6CC),
130 (0x1F6D0, 0x1F6D2),
131 (0x1F6D5, 0x1F6D7),
132 (0x1F6EB, 0x1F6EC),
133 (0x1F6F4, 0x1F6FC),
134 (0x1F7E0, 0x1F7EB),
135 (0x1F90C, 0x1F93A),
136 (0x1F93C, 0x1F945),
137 (0x1F947, 0x1F978),
138 (0x1F97A, 0x1F9CB),
139 (0x1F9CD, 0x1F9FF),
140 (0x1FA70, 0x1FA74),
141 (0x1FA78, 0x1FA7A),
142 (0x1FA80, 0x1FA86),
143 (0x1FA90, 0x1FAA8),
144 (0x1FAB0, 0x1FAB6),
145 (0x1FAC0, 0x1FAC2),
146 (0x1FAD0, 0x1FAD6),
147 (0x20000, 0x2FFFD),
148 (0x30000, 0x3FFFD),
149];
150
151pub fn char_width(c: char) -> usize {
153 let cp = c as u32;
154 if cp == 0 {
155 return 0;
156 }
157 if cp < 0x20 || (0x7F..0xA0).contains(&cp) {
158 return 0;
160 }
161 if cp < 0x300 {
162 return 1;
164 }
165 if in_ranges(ZERO, cp) {
166 return 0;
167 }
168 if in_ranges(WIDE, cp) {
169 return 2;
170 }
171 1
172}
173
174pub fn is_cjk(c: char) -> bool {
184 in_ranges(CJK, c as u32)
185}
186
187const CJK: &[(u32, u32)] = &[
190 (0x1100, 0x11FF), (0x2E80, 0x2EF3), (0x2F00, 0x2FD5), (0x3000, 0x303F), (0x3041, 0x30FF), (0x3105, 0x312F),
196 (0x3131, 0x318E), (0x3190, 0x31E3),
198 (0x31F0, 0x321E),
199 (0x3220, 0x3247),
200 (0x3250, 0x4DBF),
201 (0x4E00, 0x9FFF), (0xA960, 0xA97C), (0xAC00, 0xD7A3), (0xD7B0, 0xD7FB), (0xF900, 0xFAFF), (0xFE10, 0xFE19),
207 (0xFE30, 0xFE6B), (0xFF01, 0xFF60), (0xFF61, 0xFFDC), (0xFFE0, 0xFFE6), (0x20000, 0x2FFFD),
212 (0x30000, 0x3FFFD),
213];
214
215pub fn str_width(s: &str) -> usize {
221 s.chars().map(char_width).sum()
222}
223
224fn in_ranges(table: &[(u32, u32)], cp: u32) -> bool {
225 table
226 .binary_search_by(|&(lo, hi)| {
227 if cp < lo {
228 std::cmp::Ordering::Greater
229 } else if cp > hi {
230 std::cmp::Ordering::Less
231 } else {
232 std::cmp::Ordering::Equal
233 }
234 })
235 .is_ok()
236}
237
238#[cfg(test)]
239mod tests {
240 use super::*;
241
242 #[test]
243 fn tables_are_sorted_and_disjoint() {
244 for table in [ZERO, WIDE] {
246 for w in table.windows(2) {
247 assert!(w[0].1 < w[1].0, "구간이 겹치거나 순서가 틀렸다: {:?}", w);
248 }
249 for &(lo, hi) in table {
250 assert!(lo <= hi);
251 }
252 }
253 }
254
255 #[test]
256 fn known_widths() {
257 for (c, w) in [
258 ('a', 1),
259 ('9', 1),
260 (' ', 1),
261 ('|', 1),
262 ('가', 2),
263 ('힣', 2),
264 ('漢', 2),
265 ('あ', 2),
266 ('ア', 2),
267 (',', 2),
268 (' ', 2), ('A', 2), ('✅', 2),
271 ('🚀', 2),
272 ('\u{200b}', 0), ('\u{0301}', 0), ('\n', 0),
275 ] {
276 assert_eq!(char_width(c), w, "{c:?} 의 폭이 {w} 가 아니다");
277 }
278 }
279
280 #[test]
281 fn korean_text_is_twice_its_char_count() {
282 let s = "한글";
283 assert_eq!(s.chars().count(), 2);
284 assert_eq!(str_width(s), 4, "문자 수로 맞추면 표가 어긋난다");
285 }
286
287 #[test]
288 fn cjk_is_not_the_same_question_as_width() {
289 assert_eq!(char_width('ア'), 1);
291 assert!(is_cjk('ア'));
292 assert_eq!(char_width('🚀'), 2);
294 assert!(!is_cjk('🚀'));
295 assert!(!is_cjk('✅'));
296 assert!(is_cjk('\u{1100}') && is_cjk('\u{1161}'));
298 for c in ['¥', '₩', '£'] {
300 assert!(is_cjk(c), "{c} 가 CJK 로 안 잡힌다");
301 }
302 for c in ['。', '、', ',', '「', '」', '(', ')'] {
304 assert!(is_cjk(c), "{c} 가 CJK 로 안 잡힌다");
305 }
306 for c in ['a', '1', ' ', '.', '-'] {
308 assert!(!is_cjk(c), "{c} 가 CJK 로 잡힌다");
309 }
310 }
311
312 #[test]
313 fn cjk_table_is_sorted_and_disjoint() {
314 for w in CJK.windows(2) {
315 assert!(w[0].1 < w[1].0, "구간이 겹치거나 순서가 틀렸다: {:?}", w);
316 }
317 }
318
319 #[test]
320 fn mixed_width_adds_up() {
321 assert_eq!(str_width("환경 env"), 2 + 2 + 1 + 3);
322 }
323}