|
|
| import re
|
| from typing import List
|
|
|
| from models.city import DB, _norm
|
|
|
| CONNECT = re.compile(r"\s*(?:–|—|-|→|->|>|to)\s*", re.I)
|
| PLANE_ANY_RE = re.compile(r"\b(?:vol|flight|plane|fly)\b(?:\s*(?:pour|to|vers|à|a))?\s*([A-Za-zÀ-ỹ' \-/]{2,})", re.I)
|
| PLANE_TAIL_RE = re.compile(r"(?:^|[&;,]|-)\\s*(?:vol|flight|plane|fly)\b[^\w]*(?:to|pour|vers|à|a)\s+([A-Za-zÀ-ỹ' \-]+)", re.I)
|
| ARRIVE_RE = re.compile(r"\b(arriv(?:ee|ée|e)?|arrival)\b", re.I)
|
| DEPART_RE = re.compile(r"\b(d[ée]part|departure|return flight|vol de retour|vol depart|vol départ)\b", re.I)
|
| TRAIN_KEYS = {"train", "rail", "tàu", "tau"}
|
| BOAT_KEYS = {"boat", "cruise", "croisiere", "croisière", "ferry", "sampan", "barque", "kayak"}
|
|
|
|
|
| def find_cities_in_text(text: str) -> List[str]:
|
| return DB.match_in_text(text)
|
|
|
|
|
| def _clean_dest_token(s: str) -> str:
|
| s = s.strip()
|
| s = re.split(r"[.,;/()|]|(?:\s+-\s+)|(?:—)|(?:–)", s)[0].strip()
|
| return s
|
|
|
|
|
| def parse_itinerary(text: str):
|
| legs = []
|
| last_for_flight = None
|
| arrive_hint = None
|
| depart_hint = None
|
|
|
| for raw in (text or "").splitlines():
|
| line = raw.strip()
|
| if not line:
|
| continue
|
| if re.match(r"^\d{1,2}\s+[A-Za-z]{3}\s+\d{2,4}$", line):
|
| continue
|
|
|
| if ARRIVE_RE.search(line):
|
| cs = find_cities_in_text(line)
|
| if cs:
|
| arrive_hint = cs[0]
|
| if DEPART_RE.search(line):
|
| cs = find_cities_in_text(line)
|
| if cs:
|
| depart_hint = cs[-1]
|
|
|
| for m in PLANE_ANY_RE.finditer(line):
|
| dest_raw = _clean_dest_token(m.group(1))
|
| dests = find_cities_in_text(dest_raw)
|
| dest = dests[0] if dests else None
|
| srcs = find_cities_in_text(line[:m.start()])
|
| src = srcs[-1] if srcs else last_for_flight
|
| if src and dest and src != dest:
|
| legs.append({"a": src, "b": dest, "mode": "plane"})
|
| last_for_flight = dest
|
|
|
| def _remove_plane(seg: str) -> str:
|
| out = seg
|
| for m in PLANE_ANY_RE.finditer(seg):
|
| s, e = m.span()
|
| out = out[:s] + " " * (e - s) + out[e:]
|
| return out
|
|
|
| base_no_plane = _remove_plane(line)
|
| base_can = _norm(base_no_plane)
|
| mode = "car"
|
| if any(k in base_can for k in TRAIN_KEYS):
|
| mode = "train"
|
| elif any(k in base_can for k in BOAT_KEYS):
|
| mode = "boat"
|
|
|
| tokens = [t for t in CONNECT.split(base_no_plane) if t.strip()]
|
| cities = []
|
| for tk in tokens:
|
| found = find_cities_in_text(tk)
|
| if found:
|
| cities.extend(found)
|
|
|
| if len(cities) >= 2:
|
| for i in range(len(cities) - 1):
|
| a, b = cities[i], cities[i + 1]
|
| if a == b:
|
| continue
|
| if any(e["a"] == a and e["b"] == b and e["mode"] == "plane" for e in legs):
|
| continue
|
| legs.append({"a": a, "b": b, "mode": mode})
|
| last_for_flight = cities[-1]
|
| elif len(cities) == 1:
|
| last_for_flight = cities[-1]
|
|
|
| for m in PLANE_TAIL_RE.finditer(line):
|
| dest_raw = m.group(1).strip()
|
| dests = find_cities_in_text(dest_raw)
|
| dest = dests[0] if dests else None
|
| if dest and last_for_flight and last_for_flight != dest:
|
| if not any(e["a"] == last_for_flight and e["b"] == dest and e["mode"] == "plane" for e in legs):
|
| legs.append({"a": last_for_flight, "b": dest, "mode": "plane"})
|
| last_for_flight = dest
|
|
|
| legs = [e for e in legs if e["a"] != e["b"]]
|
| markers = {"arrive_city": arrive_hint, "depart_city": depart_hint}
|
| return legs, markers
|
|
|