eterm_parser/
pnr.rs

1use crate::util;
2
3/// The result that pnr text parsed.
4#[derive(Default, Debug)]
5pub struct Pnr<'a> {
6    pub infos: Option<Vec<&'a str>>,
7    pub ssr_items: Option<Vec<SSR<'a>>>,
8    pub osi_items: Option<Vec<OSI<'a>>>,
9    pub seg_items: Option<Vec<SEG<'a>>>,
10    pub nm_items: Option<Vec<NM<'a>>>,
11    pub rmk_items: Option<Vec<RMK<'a>>>,
12    pub other_items: Option<Vec<OtherItem<'a>>>,
13    pub is_ticket_pnr: Option<bool>,
14    pub is_cancelled_pnr: Option<bool>,
15    pub is_group_pnr: Option<bool>,
16    pub group_pnr_name: Option<&'a str>,
17    pub pax_count: Option<u8>,
18    pub pnr_code: Option<&'a str>,
19    pub bpnr_code: Option<&'a str>,
20    pub has_married_segment: Option<bool>,
21    pub office_no: Option<&'a str>,
22}
23
24impl<'a> Pnr<'a> {
25    pub fn parse(text: &'a str) -> anyhow::Result<Self> {
26        if text.is_empty() {
27            return Err(anyhow::Error::msg(
28                "pnr parameter shouldn't be empty.".to_owned(),
29            ));
30        }
31        let mut pnr = Self {
32            ..Default::default()
33        };
34
35        let mut info_parsed = false;
36        let mut index = 0u8;
37        for line in text.lines() {
38            if !info_parsed && line.starts_with(" 1.") {
39                info_parsed = true;
40            }
41            if !info_parsed {
42                if line.contains("**ELECTRONIC TICKET PNR**") {
43                    pnr.is_ticket_pnr = Some(true);
44                }
45                if line.contains("*THIS PNR WAS ENTIRELY CANCELLED*") {
46                    pnr.is_cancelled_pnr = Some(true);
47                }
48                if line.contains("MARRIED SEGMENT EXIST IN THE PNR") {
49                    pnr.has_married_segment = Some(true);
50                }
51                pnr.infos.get_or_insert(Vec::new()).push(line);
52            } else {
53                match line.trim_start() {
54                    x if x.starts_with("1.") => {
55                        let re = regex::Regex::new(r"(?<NMS>1\.(.*))(?<PNRCODE>\w{6})\s*$")?;
56                        match re.captures(line) {
57                            Some(caps) => match (caps.name("NMS"), caps.name("PNRCODE")) {
58                                (Some(nms), Some(pnrcode)) => {
59                                    pnr.pnr_code = Some(pnrcode.as_str());
60                                    if let Ok(items) = NM::parse(index, nms.as_str()) {
61                                        pnr.nm_items = Some(items);
62                                    }
63                                }
64                                _ => {}
65                            },
66                            _ => {}
67                        }
68                    }
69                    x if regex::Regex::is_match(
70                        &regex::Regex::new(
71                            r"(?<GROUPPNRNAME>.*)\s*NM(?<PAXCOUNT>\d+)\s+(?<PNRCODE>\w{6})/(\w{2})",
72                        )?,
73                        x,
74                    ) =>
75                    {
76                        pnr.is_group_pnr = Some(true);
77                        if let Some(caps) = regex::Regex::captures(
78                            &regex::Regex::new(
79                                r"(?<GROUPPNRNAME>.*)\s*NM(?<PAXCOUNT>\d+)\s+(?<PNRCODE>\w{6})/(\w{2})",
80                            )?,
81                            x,
82                        ) {
83                            match (
84                                caps.name("GROUPPNRNAME"),
85                                caps.name("PAXCOUNT"),
86                                caps.name("PNRCODE"),
87                            ) {
88                                (Some(pnr_groupname), Some(paxcount), Some(pnrcode)) => {
89                                    pnr.group_pnr_name = Some(pnr_groupname.as_str());
90                                    pnr.pnr_code = Some(pnrcode.as_str());
91                                    pnr.pax_count = paxcount.as_str().parse::<u8>().ok();
92                                }
93                                _ => {}
94                            }
95                        }
96                    }
97                    x if x.starts_with(&format!("{}. ", index)) => {
98                        if let Ok(item) = SEG::parse(index, line) {
99                            pnr.seg_items.get_or_insert(Vec::new()).push(item);
100                        }
101                    }
102                    x if x.starts_with(&format!("{}SSR", index)) => {
103                        if let Ok(item) = SSR::parse(index, line) {
104                            pnr.ssr_items.get_or_insert(Vec::new()).push(item);
105                        }
106                    }
107                    x if x.starts_with(&format!("{}OSI", index)) => {
108                        if let Ok(item) = OSI::parse(index, line) {
109                            pnr.osi_items.get_or_insert(Vec::new()).push(item);
110                        }
111                    }
112                    x if x.starts_with(&format!("{}RMK", index)) => {
113                        if let Ok(item) = RMK::parse(index, line) {
114                            pnr.rmk_items.get_or_insert(Vec::new()).push(item);
115                        }
116                    }
117                    _ => {
118                        if let Ok(item) = OtherItem::parse(index, line) {
119                            pnr.other_items.get_or_insert(Vec::new()).push(item);
120                        }
121                    }
122                }
123            }
124            index += 1;
125        }
126        Self::fix_nm(&mut pnr);
127        if pnr.pax_count.is_none() {
128            pnr.pax_count = pnr.nm_items.as_ref().and_then(|x| Some(x.len() as u8));
129        }
130        pnr.office_no = pnr.other_items.as_ref().and_then(|x| {
131            x.iter().find_map(|n| {
132                if n.item_type == "OFFICE" {
133                    Some(n.raw.trim())
134                } else {
135                    None
136                }
137            })
138        });
139        pnr.bpnr_code = pnr.rmk_items.as_ref().and_then(|x| {
140            x.iter().find_map(|n| {
141                if n.service_code.is_some_and(|s| s == "CA") {
142                    n.text.as_ref().and_then(|k| Some(&k[0..6]))
143                } else {
144                    None
145                }
146            })
147        });
148        Ok(pnr)
149    }
150
151    /// fill id info with ssr.
152    fn fix_nm(pnr: &mut Pnr) {
153        match (&pnr.ssr_items, &mut pnr.nm_items) {
154            (Some(ssrs), Some(nms)) => {
155                nms.iter_mut().for_each(|x| {
156                    if let Some(ssr) = ssrs.iter().find(|s| {
157                        s.passenger_index.is_some_and(|n| n == x.index)
158                            && s.service_code.is_some_and(|n| n == "FOID")
159                    }) {
160                        if let Some(tx) = &ssr.text {
161                            x.id_type = Some(&tx[0..2]);
162                            x.id_number = Some(&tx[2..]);
163                        }
164                    }
165                });
166            }
167            _ => {}
168        }
169    }
170}
171
172/// This is a simple item, except NM,SSR,OSI,SEG,RMK, etc.
173#[derive(Default, Debug)]
174pub struct OtherItem<'a> {
175    pub index: u8,
176    pub item_type: &'a str,
177    pub raw: &'a str,
178}
179
180impl<'a> OtherItem<'a> {
181    pub fn parse(index: u8, raw: &'a str) -> anyhow::Result<Self> {
182        let item_type = match raw {
183            x if x.starts_with(&format!("{}TL", index)) => "TL",
184            x if x.starts_with(&format!("{}FN", index)) => "FN",
185            x if x.starts_with(&format!("{}FC", index)) => "FC",
186            x if x.starts_with(&format!("{}FP", index)) => "FP",
187            x if x.starts_with(&format!("{}EI", index)) => "EI",
188            x if x.starts_with(&format!("{}XN", index)) => "XN",
189            x if x.starts_with(&format!("{}TC", index)) => "TC",
190            x if x.starts_with(&format!("{}TN/", index)) => "TN",
191            x if regex::Regex::is_match(&regex::Regex::new(r"^\d{2}\.[A-Z]{3}\d{3}$")?, x) => {
192                "OFFICE"
193            }
194            _ => "TEXT",
195        };
196        Ok(Self {
197            index,
198            item_type: item_type,
199            raw: raw,
200        })
201    }
202}
203
204/// The passenger infomation of pnr.
205#[derive(Default, Debug)]
206pub struct NM<'a> {
207    pub index: u8,
208    pub raw: &'a str,
209    pub name: Option<&'a str>,
210    //pub ssr_items: Option<Vec<SSR>>,
211    //pub osi_items: Option<Vec<OSI>>,
212    pub id_number: Option<&'a str>,
213    pub id_type: Option<&'a str>,
214}
215
216impl<'a> NM<'a> {
217    pub fn parse(index: u8, raw: &'a str) -> anyhow::Result<Vec<Self>> {
218        let re = regex::Regex::new(r"(\d+\.)")?;
219        let nms = re
220            .split(raw)
221            .filter_map(|cap| match cap.trim() {
222                x if x.is_empty() => None,
223                x if x.ends_with(".") => None,
224                x => Some(Self {
225                    index,
226                    raw: cap.trim(),
227                    name: Some(x),
228                    ..Default::default()
229                }),
230            })
231            .collect::<Vec<_>>();
232        Ok(nms)
233    }
234}
235
236/// The flight segment infomation of pnr.
237#[derive(Default, Debug)]
238pub struct SEG<'a> {
239    pub index: u8,
240    pub raw: &'a str,
241    pub org: Option<&'a str>,
242    pub dst: Option<&'a str>,
243    pub seat_class: Option<&'a str>,
244    pub flight_date: Option<&'a str>,
245    pub takeoff: Option<&'a str>,
246    pub landing: Option<&'a str>,
247    pub landing_addday: Option<u8>,
248    pub action_code: Option<&'a str>,
249    pub action_code_qty: Option<u8>,
250    pub flight_no: Option<&'a str>,
251    pub is_share: Option<bool>,
252}
253
254impl<'a> SEG<'a> {
255    pub fn parse(index: u8, raw: &'a str) -> anyhow::Result<Self> {
256        let re = regex::Regex::new(
257            r"(?<FLIGHTNO>\*?\w{5,6})\s+(?<SEATCLASS>[A-Z]\d?)\s+[A-Z]{2}(?<FLIGHTDATE>\d{2}[A-Z]{3}(?:\d{2})?)\s*(?<ORG>[A-Z]{3})(?<DST>[A-Z]{3})\s*(?<ACTIONCODE>[A-Z]{2})(?<ACTIONCODEQTY>\d{1,2})\s*(?<DEPTIME>\d{4})\s*(?<ARRTIME>\d{4})(?:\+(?<ADDDAY>\d))?",
258        )?;
259        match re.captures(raw) {
260            Some(caps) => match (
261                caps.name("FLIGHTNO"),
262                caps.name("SEATCLASS"),
263                caps.name("FLIGHTDATE"),
264                caps.name("ORG"),
265                caps.name("DST"),
266                caps.name("ACTIONCODE"),
267                caps.name("ACTIONCODEQTY"),
268                caps.name("DEPTIME"),
269                caps.name("ARRTIME"),
270                caps.name("ADDDAY"),
271            ) {
272                (
273                    Some(flight_no),
274                    Some(seat_class),
275                    Some(flight_date),
276                    Some(org),
277                    Some(dst),
278                    Some(action_code),
279                    Some(action_code_qty),
280                    Some(takeoff),
281                    Some(landing),
282                    addday,
283                ) => Ok(Self {
284                    index,
285                    raw: raw,
286                    flight_no: Some(flight_no.as_str()),
287                    seat_class: Some(seat_class.as_str()),
288                    flight_date: Some(flight_date.as_str()),
289                    org: Some(org.as_str()),
290                    dst: Some(dst.as_str()),
291                    action_code: Some(action_code.as_str()),
292                    action_code_qty: action_code_qty.as_str().parse::<u8>().ok(), // action_code_qty.and_then(|x|x.as_str().parse::<u8>().ok()),
293                    takeoff: Some(takeoff.as_str()),
294                    landing: Some(landing.as_str()),
295                    landing_addday: util::regex_extact_value::<u8>(addday), // passenger_index.and_then(|x|x.as_str().parse::<u8>().ok()),
296                    is_share: Some(flight_no.as_str().starts_with("*")),
297                }),
298                _ => Ok(Self {
299                    index,
300                    raw: raw,
301                    ..Default::default()
302                }),
303            },
304            _ => Ok(Self {
305                index,
306                raw: raw,
307                ..Default::default()
308            }),
309        }
310    }
311}
312
313/// The ssr infomation of pnr.
314#[derive(Default, Debug)]
315pub struct SSR<'a> {
316    pub index: u8,
317    pub raw: &'a str,
318    pub service_code: Option<&'a str>,
319    pub action_code: Option<&'a str>,
320    pub action_code_qty: Option<u8>,
321    pub airline: Option<&'a str>,
322    pub text: Option<&'a str>,
323    pub passenger_index: Option<u8>,
324    pub segment_index: Option<u8>,
325}
326
327impl<'a> SSR<'a> {
328    pub fn parse(index: u8, raw: &'a str) -> anyhow::Result<Self> {
329        let re = regex::Regex::new(
330            r"SSR (?<SERVICECODE>[A-Z]+) (?<AIRLINE>\w{2}) (?:(?<ACTIONCODE>\w{2})(?<ACTIONCODEQTY>\d|/+)?\s+)?(?<TEXT>[^\r\n]*(?<!/P\d+)(?<!/S\d+))(/P(?<PASSENGERINDEX>\d+))?(/S(?<SEGMENTINDEX>\d+))?\s*$",
331        )?;
332        match re.captures(raw) {
333            Some(caps) => match (
334                caps.name("SERVICECODE"),
335                caps.name("AIRLINE"),
336                caps.name("ACTIONCODE"),
337                caps.name("ACTIONCODEQTY"),
338                caps.name("TEXT"),
339                caps.name("PASSENGERINDEX"),
340                caps.name("SEGMENTINDEX"),
341            ) {
342                (
343                    Some(service_code),
344                    Some(airline),
345                    action_code,
346                    action_code_qty,
347                    Some(text),
348                    passenger_index,
349                    segment_index,
350                ) => Ok(Self {
351                    index,
352                    raw: raw,
353                    service_code: Some(service_code.as_str()),
354                    airline: Some(airline.as_str()),
355                    action_code: action_code.map(|x| x.as_str()),
356                    action_code_qty: util::regex_extact_value::<u8>(action_code_qty),
357                    text: Some(text.as_str()),
358                    passenger_index: util::regex_extact_value::<u8>(passenger_index),
359                    segment_index: util::regex_extact_value::<u8>(segment_index),
360                }),
361                _ => Ok(Self {
362                    index,
363                    raw: raw,
364                    ..Default::default()
365                }),
366            },
367            _ => Ok(Self {
368                index,
369                raw: raw,
370                ..Default::default()
371            }),
372        }
373    }
374}
375
376/// The osi infomation of pnr.
377#[derive(Default, Debug)]
378pub struct OSI<'a> {
379    pub index: u8,
380    pub raw: &'a str,
381    pub service_code: Option<&'a str>,
382    pub airline: Option<&'a str>,
383    pub text: Option<&'a str>,
384    pub passenger_index: Option<u8>,
385}
386
387impl<'a> OSI<'a> {
388    pub fn parse(index: u8, raw: &'a str) -> anyhow::Result<Self> {
389        let re = regex::Regex::new(
390            r"OSI (?<AIRLINE>\w{2}) (?<SERVICECODE>[A-Z]+)?(?<TEXT>(.*(?<!/P\d+)))(/P(?<PASSENGERINDEX>\d+))?$",
391        )?;
392        match re.captures(raw) {
393            Some(caps) => match (
394                caps.name("AIRLINE"),
395                caps.name("SERVICECODE"),
396                caps.name("TEXT"),
397                caps.name("PASSENGERINDEX"),
398            ) {
399                (Some(airline), Some(service_code), Some(text), passenger_index) => Ok(Self {
400                    index,
401                    raw: raw,
402                    service_code: Some(service_code.as_str()),
403                    airline: Some(airline.as_str()),
404                    text: Some(text.as_str()),
405                    passenger_index: util::regex_extact_value::<u8>(passenger_index), // passenger_index.and_then(|x|x.as_str().parse::<u8>().ok()),
406                }),
407                _ => Ok(Self {
408                    index,
409                    raw: raw,
410                    ..Default::default()
411                }),
412            },
413            _ => Ok(Self {
414                index,
415                raw: raw,
416                ..Default::default()
417            }),
418        }
419    }
420}
421
422/// The remark infomation of pnr.
423#[derive(Default, Debug)]
424pub struct RMK<'a> {
425    pub index: u8,
426    pub raw: &'a str,
427    pub service_code: Option<&'a str>,
428    pub text: Option<&'a str>,
429    pub passenger_index: Option<u8>,
430}
431
432impl<'a> RMK<'a> {
433    pub fn parse(index: u8, raw: &'a str) -> anyhow::Result<Self> {
434        let re = regex::Regex::new(
435            r"RMK[ :/](?<SERVICECODE>(MP|TJ AUTH|CA|CID|TID|EMAIL|1A|GMJC|RV|ORI))[ :/](?<TEXT>.*?)(/P(?<PASSENGERINDEX>\d))?$",
436        )?;
437        match re.captures(raw) {
438            Some(caps) => match (
439                caps.name("SERVICECODE"),
440                caps.name("TEXT"),
441                caps.name("PASSENGERINDEX"),
442            ) {
443                (Some(service_code), text, passenger_index) => Ok(Self {
444                    index,
445                    raw: raw,
446                    service_code: Some(service_code.as_str()),
447                    text: util::regex_extact_text(text),
448                    passenger_index: util::regex_extact_value::<u8>(passenger_index),
449                }),
450                _ => Ok(Self {
451                    index,
452                    raw: raw,
453                    ..Default::default()
454                }),
455            },
456            _ => Ok(Self {
457                index,
458                raw: raw,
459                ..Default::default()
460            }),
461        }
462    }
463}