Skip to main content

amlich_core/almanac/recommendation/
activity.rs

1use 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}