1use alloc::collections::BTreeMap;
56use alloc::string::String;
57use alloc::vec::Vec;
58use core::cmp::Reverse;
59
60pub struct AbbrevMap {
69 map: BTreeMap<String, Vec<String>>,
71 sorted: Vec<(String, String)>,
76}
77
78impl AbbrevMap {
79 pub fn empty() -> Self {
81 Self {
82 map: BTreeMap::new(),
83 sorted: Vec::new(),
84 }
85 }
86
87 pub fn builtin() -> Self {
93 Self::from_tsv(include_str!("../data/abbrev_th.tsv"))
94 }
95
96 pub fn from_tsv(data: &str) -> Self {
104 let mut map: BTreeMap<String, Vec<String>> = BTreeMap::new();
105
106 for line in data.lines() {
107 let line = line.trim();
108 if line.is_empty() || line.starts_with('#') {
109 continue;
110 }
111 let mut cols = line.split('\t');
112 let key = match cols.next() {
113 Some(k) if !k.is_empty() => String::from(k),
114 _ => continue,
115 };
116 let expansions: Vec<String> = cols
117 .map(str::trim)
118 .filter(|s| !s.is_empty())
119 .map(String::from)
120 .collect();
121 if expansions.is_empty() {
122 continue;
123 }
124 map.entry(key).or_default().extend(expansions);
125 }
126
127 let mut sorted: Vec<(String, String)> =
130 map.iter().map(|(k, v)| (k.clone(), v[0].clone())).collect();
131 sorted.sort_by_key(|pair| Reverse(pair.0.len()));
133
134 Self { map, sorted }
135 }
136
137 pub fn lookup(&self, abbrev: &str) -> Option<&[String]> {
155 self.map.get(abbrev).map(Vec::as_slice)
156 }
157
158 pub fn expand_text(&self, text: &str) -> String {
183 if self.sorted.is_empty() || text.is_empty() {
184 return String::from(text);
185 }
186
187 let mut result = String::with_capacity(text.len());
188 let mut i = 0usize; 'outer: while i < text.len() {
191 let remaining = &text[i..];
192 for (key, expansion) in &self.sorted {
193 if remaining.starts_with(key.as_str()) {
194 result.push_str(expansion);
195 i += key.len();
196 continue 'outer;
197 }
198 }
199 let c = remaining.chars().next().unwrap();
201 result.push(c);
202 i += c.len_utf8();
203 }
204
205 result
206 }
207
208 pub fn len(&self) -> usize {
210 self.map.len()
211 }
212
213 pub fn is_empty(&self) -> bool {
215 self.map.is_empty()
216 }
217}
218
219#[cfg(test)]
224mod tests {
225 use super::*;
226
227 fn mini() -> AbbrevMap {
228 AbbrevMap::from_tsv("ก.ค.\tกรกฎาคม\nพ.ศ.\tพุทธศักราช\nดร.\tดอกเตอร์\nศ.ดร.\tศาสตราจารย์ดอกเตอร์\nศ.\tศาสตราจารย์\nอ.\tอาจารย์\tอำเภอ\n")
229 }
230
231 #[test]
234 fn empty_has_no_entries() {
235 let m = AbbrevMap::empty();
236 assert!(m.is_empty());
237 assert_eq!(m.len(), 0);
238 }
239
240 #[test]
241 fn from_tsv_parses_entries() {
242 let m = mini();
243 assert!(!m.is_empty());
244 assert!(m.len() >= 5);
245 }
246
247 #[test]
248 fn from_tsv_skips_comments_and_blanks() {
249 let m = AbbrevMap::from_tsv("# comment\n\nก.ค.\tกรกฎาคม\n");
250 assert_eq!(m.len(), 1);
251 }
252
253 #[test]
254 fn from_tsv_skips_lines_without_expansion() {
255 let m = AbbrevMap::from_tsv("ก.ค.\n");
256 assert_eq!(m.len(), 0);
257 }
258
259 #[test]
260 fn from_tsv_duplicate_keys_merge() {
261 let m = AbbrevMap::from_tsv("อ.\tอาจารย์\nอ.\tอำเภอ\n");
262 let exps = m.lookup("อ.").unwrap();
263 assert!(exps.contains(&String::from("อาจารย์")));
264 assert!(exps.contains(&String::from("อำเภอ")));
265 }
266
267 #[test]
270 fn lookup_known_key() {
271 let m = mini();
272 let exps = m.lookup("ก.ค.").expect("ก.ค. should be in map");
273 assert_eq!(exps, &[String::from("กรกฎาคม")]);
274 }
275
276 #[test]
277 fn lookup_unknown_key_returns_none() {
278 let m = mini();
279 assert_eq!(m.lookup("xyz"), None);
280 }
281
282 #[test]
283 fn lookup_ambiguous_returns_all() {
284 let m = mini();
285 let exps = m.lookup("อ.").unwrap();
286 assert!(exps.contains(&String::from("อาจารย์")));
287 assert!(exps.contains(&String::from("อำเภอ")));
288 }
289
290 #[test]
293 fn expand_single_abbreviation() {
294 let m = mini();
295 assert_eq!(m.expand_text("ก.ค."), "กรกฎาคม");
296 }
297
298 #[test]
299 fn expand_in_context() {
300 let m = mini();
301 assert_eq!(m.expand_text("5ก.ค.2567"), "5กรกฎาคม2567");
302 }
303
304 #[test]
305 fn expand_multiple_abbreviations() {
306 let m = mini();
307 assert_eq!(m.expand_text("พ.ศ.2567ก.ค."), "พุทธศักราช2567กรกฎาคม");
308 }
309
310 #[test]
311 fn expand_no_match_returns_original() {
312 let m = mini();
313 assert_eq!(m.expand_text("ไม่มีอะไร"), "ไม่มีอะไร");
314 }
315
316 #[test]
317 fn expand_empty_input() {
318 let m = mini();
319 assert_eq!(m.expand_text(""), "");
320 }
321
322 #[test]
323 fn expand_greedy_longest_first() {
324 let m = mini();
326 assert_eq!(m.expand_text("ศ.ดร.สมชาย"), "ศาสตราจารย์ดอกเตอร์สมชาย");
327 }
328
329 #[test]
330 fn expand_shorter_key_after_no_long_match() {
331 let m = mini();
333 assert_eq!(m.expand_text("ศ.สมชาย"), "ศาสตราจารย์สมชาย");
334 }
335
336 #[test]
337 fn expand_empty_map_returns_original() {
338 let m = AbbrevMap::empty();
339 assert_eq!(m.expand_text("ก.ค."), "ก.ค.");
340 }
341
342 #[test]
345 fn builtin_has_all_months() {
346 let m = AbbrevMap::builtin();
347 let months = [
348 ("ม.ค.", "มกราคม"),
349 ("ก.พ.", "กุมภาพันธ์"),
350 ("มี.ค.", "มีนาคม"),
351 ("เม.ย.", "เมษายน"),
352 ("พ.ค.", "พฤษภาคม"),
353 ("มิ.ย.", "มิถุนายน"),
354 ("ก.ค.", "กรกฎาคม"),
355 ("ส.ค.", "สิงหาคม"),
356 ("ก.ย.", "กันยายน"),
357 ("ต.ค.", "ตุลาคม"),
358 ("พ.ย.", "พฤศจิกายน"),
359 ("ธ.ค.", "ธันวาคม"),
360 ];
361 for (abbr, expected) in months {
362 let exps = m
363 .lookup(abbr)
364 .unwrap_or_else(|| panic!("{abbr} missing from builtin"));
365 assert_eq!(
366 exps[0], expected,
367 "primary expansion of {abbr} should be {expected}"
368 );
369 }
370 }
371
372 #[test]
373 fn builtin_has_era_markers() {
374 let m = AbbrevMap::builtin();
375 assert!(m.lookup("พ.ศ.").is_some());
376 assert!(m.lookup("ค.ศ.").is_some());
377 }
378
379 #[test]
380 fn builtin_expands_date_sentence() {
381 let m = AbbrevMap::builtin();
382 let result = m.expand_text("วันที่5ก.ค.พ.ศ.2567");
383 assert!(result.contains("กรกฎาคม"), "got: {result}");
384 assert!(result.contains("พุทธศักราช"), "got: {result}");
385 }
386
387 #[test]
390 fn october_matches_before_tambon() {
391 let m = AbbrevMap::builtin();
393 let result = m.expand_text("ต.ค.นี้");
394 assert_eq!(result, "ตุลาคมนี้", "got: {result}");
395 }
396
397 #[test]
398 fn tambon_matches_when_not_october() {
399 let m = AbbrevMap::builtin();
400 let result = m.expand_text("ต.สุขุมวิท");
402 assert_eq!(result, "ตำบลสุขุมวิท", "got: {result}");
403 }
404
405 #[test]
408 fn police_generals_expand() {
409 let m = AbbrevMap::builtin();
410 assert_eq!(m.expand_text("พล.ต.อ.วิชัย"), "พลตำรวจเอกวิชัย");
411 assert_eq!(m.expand_text("พล.ต.ท.สมชาย"), "พลตำรวจโทสมชาย");
412 assert_eq!(m.expand_text("พล.ต.ต.สมศรี"), "พลตำรวจตรีสมศรี");
413 }
414
415 #[test]
416 fn police_officers_expand() {
417 let m = AbbrevMap::builtin();
418 assert_eq!(m.expand_text("พ.ต.อ.ณรงค์"), "พันตำรวจเอกณรงค์");
419 assert_eq!(m.expand_text("ร.ต.อ.มานะ"), "ร้อยตำรวจเอกมานะ");
420 assert_eq!(m.expand_text("ด.ต.ประสิทธิ์"), "ดาบตำรวจประสิทธิ์");
421 }
422
423 #[test]
424 fn police_ncos_expand() {
425 let m = AbbrevMap::builtin();
426 assert_eq!(m.expand_text("ส.ต.อ.บุญมี"), "สิบตำรวจเอกบุญมี");
427 assert_eq!(m.expand_text("ส.ต.ต.สุรชัย"), "สิบตำรวจตรีสุรชัย");
428 }
429
430 #[test]
433 fn army_generals_expand() {
434 let m = AbbrevMap::builtin();
435 assert_eq!(m.expand_text("พล.อ.ประยุทธ์"), "พลเอกประยุทธ์");
436 assert_eq!(m.expand_text("พล.ท.สกล"), "พลโทสกล");
437 assert_eq!(m.expand_text("พล.ต.ชาติ"), "พลตรีชาติ");
438 }
439
440 #[test]
441 fn army_officers_expand() {
442 let m = AbbrevMap::builtin();
443 assert_eq!(m.expand_text("พ.อ.วีระ"), "พันเอกวีระ");
444 assert_eq!(m.expand_text("ร.อ.ธนู"), "ร้อยเอกธนู");
445 assert_eq!(m.expand_text("จ.ส.อ.สมพร"), "จ่าสิบเอกสมพร");
446 }
447
448 #[test]
451 fn police_longer_form_shadows_army_shorter_form() {
452 let m = AbbrevMap::builtin();
453 let result = m.expand_text("พล.ต.อ.สมบัติ");
455 assert_eq!(result, "พลตำรวจเอกสมบัติ", "got: {result}");
456 let result2 = m.expand_text("พล.ต.วิเชียร");
458 assert_eq!(result2, "พลตรีวิเชียร", "got: {result2}");
459 }
460
461 #[test]
462 fn พตอ_is_police_not_army() {
463 let m = AbbrevMap::builtin();
464 assert_eq!(m.expand_text("พ.ต.อ.กล้า"), "พันตำรวจเอกกล้า");
465 assert_eq!(m.expand_text("พ.ต.ดำ"), "พันตรีดำ");
467 }
468
469 #[test]
472 fn navy_admirals_expand() {
473 let m = AbbrevMap::builtin();
474 assert_eq!(m.expand_text("พล.ร.อ.ชุมพล"), "พลเรือเอกชุมพล");
475 assert_eq!(m.expand_text("พล.ร.ต.สิทธิ"), "พลเรือตรีสิทธิ");
476 }
477
478 #[test]
479 fn airforce_generals_shadow_army_พลอ() {
480 let m = AbbrevMap::builtin();
481 assert_eq!(m.expand_text("พล.อ.อ.ประจิน"), "พลอากาศเอกประจิน");
483 assert_eq!(m.expand_text("พล.อ.ท.มานัต"), "พลอากาศโทมานัต");
484 }
485
486 #[test]
489 fn state_enterprises_expand() {
490 let m = AbbrevMap::builtin();
491 assert_eq!(m.lookup("กฟผ.").unwrap()[0], "การไฟฟ้าฝ่ายผลิตแห่งประเทศไทย");
492 assert_eq!(m.lookup("กฟน.").unwrap()[0], "การไฟฟ้านครหลวง");
493 assert_eq!(m.lookup("กฟภ.").unwrap()[0], "การไฟฟ้าส่วนภูมิภาค");
494 assert_eq!(m.lookup("รฟท.").unwrap()[0], "การรถไฟแห่งประเทศไทย");
495 assert_eq!(
496 m.lookup("รฟม.").unwrap()[0],
497 "การรถไฟฟ้าขนส่งมวลชนแห่งประเทศไทย"
498 );
499 assert_eq!(m.lookup("ปตท.").unwrap()[0], "การปิโตรเลียมแห่งประเทศไทย");
500 }
501
502 #[test]
503 fn banking_agencies_expand() {
504 let m = AbbrevMap::builtin();
505 assert_eq!(
506 m.lookup("ธ.ก.ส.").unwrap()[0],
507 "ธนาคารเพื่อการเกษตรและสหกรณ์การเกษตร"
508 );
509 assert_eq!(m.lookup("ธอส.").unwrap()[0], "ธนาคารอาคารสงเคราะห์");
510 assert_eq!(m.lookup("กบข.").unwrap()[0], "กองทุนบำเหน็จบำนาญข้าราชการ");
511 }
512
513 #[test]
516 fn government_agencies_expand() {
517 let m = AbbrevMap::builtin();
518 assert_eq!(m.lookup("กทม.").unwrap()[0], "กรุงเทพมหานคร");
519 assert_eq!(m.lookup("กกต.").unwrap()[0], "คณะกรรมการการเลือกตั้ง");
520 assert_eq!(
521 m.lookup("ป.ป.ช.").unwrap()[0],
522 "คณะกรรมการป้องกันและปราบปรามการทุจริตแห่งชาติ"
523 );
524 assert_eq!(
525 m.lookup("สสส.").unwrap()[0],
526 "สำนักงานกองทุนสนับสนุนการสร้างเสริมสุขภาพ"
527 );
528 }
529
530 #[test]
531 fn กทม_expands_in_text() {
532 let m = AbbrevMap::builtin();
533 assert_eq!(m.expand_text("กทม."), "กรุงเทพมหานคร");
534 assert_eq!(m.expand_text("ผู้ว่ากทม."), "ผู้ว่ากรุงเทพมหานคร");
535 }
536}