amlich_core/almanac/recommendation/
activity.rs1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
4#[serde(rename_all = "snake_case")]
5pub enum ActivityCategory {
6 Social,
7 Business,
8 Construction,
9 Relocation,
10 Ritual,
11 Health,
12 Legal,
13 Maintenance,
14}
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
17#[serde(rename_all = "snake_case")]
18pub enum ActivityId {
19 Travel,
20 MeetingSocial,
21 OpeningStart,
22 ContractAgreement,
23 BusinessTrade,
24 FinanceInvestment,
25 ConstructionGroundbreaking,
26 RepairRenovation,
27 MoveRelocation,
28 WeddingEngagement,
29 LawsuitDispute,
30 PrayerOffering,
31 MedicalTreatment,
32 BurialMemorial,
33 CleaningPurging,
34}
35
36impl ActivityId {
37 pub const ALL: [ActivityId; 15] = [
38 ActivityId::Travel,
39 ActivityId::MeetingSocial,
40 ActivityId::OpeningStart,
41 ActivityId::ContractAgreement,
42 ActivityId::BusinessTrade,
43 ActivityId::FinanceInvestment,
44 ActivityId::ConstructionGroundbreaking,
45 ActivityId::RepairRenovation,
46 ActivityId::MoveRelocation,
47 ActivityId::WeddingEngagement,
48 ActivityId::LawsuitDispute,
49 ActivityId::PrayerOffering,
50 ActivityId::MedicalTreatment,
51 ActivityId::BurialMemorial,
52 ActivityId::CleaningPurging,
53 ];
54
55 pub fn category(self) -> ActivityCategory {
56 match self {
57 ActivityId::Travel => ActivityCategory::Social,
58 ActivityId::MeetingSocial => ActivityCategory::Social,
59 ActivityId::OpeningStart => ActivityCategory::Business,
60 ActivityId::ContractAgreement => ActivityCategory::Business,
61 ActivityId::BusinessTrade => ActivityCategory::Business,
62 ActivityId::FinanceInvestment => ActivityCategory::Business,
63 ActivityId::ConstructionGroundbreaking => ActivityCategory::Construction,
64 ActivityId::RepairRenovation => ActivityCategory::Maintenance,
65 ActivityId::MoveRelocation => ActivityCategory::Relocation,
66 ActivityId::WeddingEngagement => ActivityCategory::Ritual,
67 ActivityId::LawsuitDispute => ActivityCategory::Legal,
68 ActivityId::PrayerOffering => ActivityCategory::Ritual,
69 ActivityId::MedicalTreatment => ActivityCategory::Health,
70 ActivityId::BurialMemorial => ActivityCategory::Ritual,
71 ActivityId::CleaningPurging => ActivityCategory::Maintenance,
72 }
73 }
74
75 pub fn labels(self) -> ActivityLabel {
76 match self {
77 ActivityId::Travel => ActivityLabel::new("Xuất hành", "Travel"),
78 ActivityId::MeetingSocial => ActivityLabel::new("Gặp gỡ", "Meetings and social visits"),
79 ActivityId::OpeningStart => ActivityLabel::new("Khai mở", "Opening and launching"),
80 ActivityId::ContractAgreement => {
81 ActivityLabel::new("Ký kết", "Contracts and agreements")
82 }
83 ActivityId::BusinessTrade => ActivityLabel::new("Giao dịch", "Business and trade"),
84 ActivityId::FinanceInvestment => {
85 ActivityLabel::new("Tài chính lớn", "Finance and investment")
86 }
87 ActivityId::ConstructionGroundbreaking => {
88 ActivityLabel::new("Động thổ", "Groundbreaking and construction")
89 }
90 ActivityId::RepairRenovation => ActivityLabel::new("Tu sửa", "Repair and renovation"),
91 ActivityId::MoveRelocation => {
92 ActivityLabel::new("Nhập trạch", "Relocation and move-in")
93 }
94 ActivityId::WeddingEngagement => {
95 ActivityLabel::new("Cưới hỏi", "Wedding and engagement")
96 }
97 ActivityId::LawsuitDispute => {
98 ActivityLabel::new("Kiện tụng", "Litigation and disputes")
99 }
100 ActivityId::PrayerOffering => ActivityLabel::new("Cầu cúng", "Prayer and offerings"),
101 ActivityId::MedicalTreatment => ActivityLabel::new("Chữa bệnh", "Medical treatment"),
102 ActivityId::BurialMemorial => {
103 ActivityLabel::new("An táng", "Burial and memorial rites")
104 }
105 ActivityId::CleaningPurging => {
106 ActivityLabel::new("Dọn dẹp", "Cleaning and purification")
107 }
108 }
109 }
110}
111
112#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
113pub struct ActivityLabel {
114 pub vi: String,
115 pub en: String,
116}
117
118impl ActivityLabel {
119 pub fn new(vi: &str, en: &str) -> Self {
120 Self {
121 vi: vi.to_string(),
122 en: en.to_string(),
123 }
124 }
125}
126
127#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
128pub struct NormalizedActivity {
129 pub activity_id: ActivityId,
130 pub label: ActivityLabel,
131 pub matched_alias: String,
132}
133
134pub fn normalize_activity_alias(input: &str) -> Option<NormalizedActivity> {
135 let normalized = canonicalize_alias(input);
136
137 let activity_id = match normalized.as_str() {
138 "xuat hanh" | "xuat hanh xa" | "di lai" | "di chuyen" | "di xa" | "travel" => {
139 ActivityId::Travel
140 }
141
142 "gap go" | "giao tiep" | "hoi hop" | "giai hoa" | "reconciliation" => {
143 ActivityId::MeetingSocial
144 }
145
146 "khai truong"
147 | "khai truong nho"
148 | "khai mo"
149 | "khoi dau"
150 | "khoi su moi"
151 | "ra mat"
152 | "ra mat khai truong"
153 | "grand opening"
154 | "small openings"
155 | "starting new ventures" => ActivityId::OpeningStart,
156
157 "ky hop dong" | "ky ket" | "signing contracts" | "signing" | "contract signing"
158 | "hop dong" => ActivityId::ContractAgreement,
159
160 "giao dich" | "business deals" | "business" | "trade" | "giao thuong" => {
161 ActivityId::BusinessTrade
162 }
163
164 "cau tai"
165 | "thu no"
166 | "giao dich tai chinh"
167 | "financial transactions"
168 | "seeking wealth"
169 | "tai chinh"
170 | "dau tu"
171 | "investment" => ActivityId::FinanceInvestment,
172
173 "dong tho" | "khoi cong" | "xay dung" | "groundbreaking" | "starting construction" => {
174 ActivityId::ConstructionGroundbreaking
175 }
176
177 "tu sua" | "tu sua nha" | "bao tri" | "sua chua" | "tu sua noi bo" | "repairs"
178 | "maintenance" | "home repairs" | "internal repairs" => ActivityId::RepairRenovation,
179
180 "nhap trach" | "move in" | "moving in" | "di doi" | "chuyen nha" => {
181 ActivityId::MoveRelocation
182 }
183
184 "cuoi hoi" | "wedding" | "dam hoi" | "dinh hon" => ActivityId::WeddingEngagement,
185
186 "kien tung" | "tranh chap" | "litigation" | "lawsuit" => ActivityId::LawsuitDispute,
187
188 "cau nguyen" | "cung te" | "cau cung" | "prayer" | "rituals" | "offerings" => {
189 ActivityId::PrayerOffering
190 }
191
192 "chua benh" | "medical treatment" | "healing" => ActivityId::MedicalTreatment,
193
194 "an tang" | "burial" | "tang le" | "memorial" => ActivityId::BurialMemorial,
195
196 "don dep"
197 | "don dep cu"
198 | "tay ue"
199 | "giai tru"
200 | "purification"
201 | "cleaning"
202 | "clearing old things" => ActivityId::CleaningPurging,
203
204 _ => return None,
205 };
206
207 Some(NormalizedActivity {
208 activity_id,
209 label: activity_id.labels(),
210 matched_alias: normalized,
211 })
212}
213
214fn canonicalize_alias(input: &str) -> String {
215 let lowered = input.trim().to_lowercase();
216 let mut out = String::with_capacity(lowered.len());
217
218 for ch in lowered.chars() {
219 let mapped = match ch {
220 'à' | 'á' | 'ạ' | 'ả' | 'ã' | 'ă' | 'ằ' | 'ắ' | 'ặ' | 'ẳ' | 'ẵ' | 'â' | 'ầ' | 'ấ'
221 | 'ậ' | 'ẩ' | 'ẫ' => 'a',
222 'đ' => 'd',
223 'è' | 'é' | 'ẹ' | 'ẻ' | 'ẽ' | 'ê' | 'ề' | 'ế' | 'ệ' | 'ể' | 'ễ' => {
224 'e'
225 }
226 'ì' | 'í' | 'ị' | 'ỉ' | 'ĩ' => 'i',
227 'ò' | 'ó' | 'ọ' | 'ỏ' | 'õ' | 'ô' | 'ồ' | 'ố' | 'ộ' | 'ổ' | 'ỗ' | 'ơ' | 'ờ' | 'ớ'
228 | 'ợ' | 'ở' | 'ỡ' => 'o',
229 'ù' | 'ú' | 'ụ' | 'ủ' | 'ũ' | 'ư' | 'ừ' | 'ứ' | 'ự' | 'ử' | 'ữ' => {
230 'u'
231 }
232 'ỳ' | 'ý' | 'ỵ' | 'ỷ' | 'ỹ' => 'y',
233 ch if ch.is_ascii_alphanumeric() => ch,
234 ch if ch.is_whitespace() || ch == '-' || ch == '_' || ch == '/' => ' ',
235 _ => ' ',
236 };
237 out.push(mapped);
238 }
239
240 out.split_whitespace().collect::<Vec<_>>().join(" ")
241}
242
243#[cfg(test)]
244mod tests {
245 use super::*;
246
247 #[test]
248 fn normalizes_common_vietnamese_aliases() {
249 let activity = normalize_activity_alias("Động thổ").expect("known alias");
250 assert_eq!(activity.activity_id, ActivityId::ConstructionGroundbreaking);
251
252 let activity = normalize_activity_alias("Ký hợp đồng").expect("known alias");
253 assert_eq!(activity.activity_id, ActivityId::ContractAgreement);
254
255 let activity = normalize_activity_alias("Xuất hành xa").expect("known alias");
256 assert_eq!(activity.activity_id, ActivityId::Travel);
257 }
258
259 #[test]
260 fn normalizes_english_aliases() {
261 let activity = normalize_activity_alias("Grand opening").expect("known alias");
262 assert_eq!(activity.activity_id, ActivityId::OpeningStart);
263
264 let activity = normalize_activity_alias("Medical treatment").expect("known alias");
265 assert_eq!(activity.activity_id, ActivityId::MedicalTreatment);
266 }
267
268 #[test]
269 fn unknown_alias_returns_none() {
270 assert!(normalize_activity_alias("Đọc sách").is_none());
271 }
272}