HEX
Server: Microsoft-IIS/10.0
System: Windows NT ITPWINWEBSVR22 10.0 build 20348 (Windows Server 2022) AMD64
User: www.conferencesearch.co.uk (0)
PHP: 8.3.30
Disabled: NONE
Upload Files
File: D:/web/hyperflight/apps/inxpress/app/filters.py
from decimal import Decimal, InvalidOperation
from markupsafe import Markup, escape
import re

def nz(val, default=""):
    return default if val is None else val

def blank_if_zero(val):
    try:
        d = Decimal(str(val))
        return "" if d == 0 else f"{d:.2f}"
    except (InvalidOperation, TypeError, ValueError):
        return ""

def fmt_num_always(val):
    try:
        return f"{Decimal(str(val)):.2f}"
    except (InvalidOperation, TypeError, ValueError):
        return "0.00"

def iif(cond, a, b):
    return a if cond else b

def fmt_date(d):
    try:
        return d.strftime("%Y-%m-%d")
    except Exception:
        return ""
        
# --- add below existing filters ---
def flag(cc):
    """Return emoji flag from 2-letter ISO code, e.g. 'GB' -> 🇬🇧 ."""
    if not cc or len(cc) != 2:
        return ""
    try:
        a, b = cc.upper()
        return chr(0x1F1E6 + (ord(a) - ord('A'))) + chr(0x1F1E6 + (ord(b) - ord('A')))
    except Exception:
        return ""

def trim2(val):
    """Format number with up to 2 dp, but strip trailing .0/.00 (e.g., 12.00 -> 12, 12.30 -> 12.3)."""
    from decimal import Decimal, InvalidOperation
    try:
        s = f"{Decimal(str(val)):.2f}"
        s = s.rstrip("0").rstrip(".")
        return s
    except (InvalidOperation, TypeError, ValueError):
        return ""
        

# --- add below existing filters and alongside flag()/trim2() ---

_COUNTRY_NAMES = {
    "GB": "United Kingdom", "UK": "United Kingdom", "IE": "Ireland",
    "FR": "France", "DE": "Germany", "ES": "Spain", "IT": "Italy",
    "NL": "Netherlands", "BE": "Belgium", "DK": "Denmark", "SE": "Sweden",
    "NO": "Norway", "FI": "Finland", "AT": "Austria", "CH": "Switzerland",
    "PL": "Poland", "PT": "Portugal", "CZ": "Czechia", "SK": "Slovakia",
    "HU": "Hungary", "RO": "Romania", "BG": "Bulgaria", "EE": "Estonia",
    "LV": "Latvia", "LT": "Lithuania",
    "US": "United States", "CA": "Canada", "MX": "Mexico",
    "AU": "Australia", "NZ": "New Zealand",
    "JP": "Japan", "KR": "South Korea", "CN": "China", "HK": "Hong Kong",
    "SG": "Singapore", "AE": "United Arab Emirates", "IN": "India",
    "PK": "Pakistan", "TR": "Türkiye", "ZA": "South Africa",
}

_COUNTRY_NAMES.update({
    "BR": "Brazil",
    "IL": "Israel",
    "GR": "Greece",
    "LB": "Lebanon",
    "LU": "Luxembourg",
    "SI": "Slovenia",  
    "UA": "Ukraine",
})


def country_name(cc):
    if not cc:
        return ""
    return _COUNTRY_NAMES.get(cc.upper(), cc.upper())

def fmt_date(d):
    if not d: return ""
    try:
        # d might be date or str 'YYYY-MM-DD'
        from datetime import datetime, date
        if isinstance(d, (datetime, )):
            return d.strftime("%d/%m/%Y")
        if isinstance(d, date):
            return d.strftime("%d/%m/%Y")
        # string fallback
        return datetime.strptime(str(d), "%Y-%m-%d").strftime("%d/%m/%Y")
    except Exception:
        return str(d)
# register: app.jinja_env.filters["fmt_date"] = fmt_date

def nl2br(s):
    if s is None:
        return ""
    return Markup("<br>".join(escape(str(s)).splitlines()))


import re
from decimal import Decimal

_amount_in_token = re.compile(r"£\s*([+\-]?\d+(?:\.\d{1,2})?)")   # finds £ 12.34
_number_token    = re.compile(r"^[+\-]?\d+(?:\.\d{1,2})$")       # bare number like 65.92

def parse_charges(raw: str):
    """
    Parse '...; Express - Package £; 65.92; Fuel £ 32.23; ...'
    into [{'label': 'Express - Package', 'amount': Decimal('65.92')}, ...].

    Rules:
    - Tokens are separated by ';'
    - We accumulate label fragments until we see an amount
      (either '£ 12.34' in the same token OR a bare number in the next token,
       especially after a token that ends with '£').
    - Unparsed leftovers become entries with amount=None (so nothing is lost).
    """
    items = []
    if not raw:
        return items

    parts = [p.strip() for p in re.split(r";\s*", str(raw).strip()) if p.strip()]
    label_buf = []          # accumulated label parts
    expecting_bare_amount = False  # if previous token ended with '£'

    def flush_unmatched_label():
        nonlocal label_buf
        if label_buf:
            items.append({"label": " ".join(label_buf).strip(), "amount": None})
            label_buf = []

    for tok in parts:
        m = _amount_in_token.search(tok)
        if m:
            # amount is inside this token; split label/amount
            amount = Decimal(m.group(1))
            # remove everything from the currency sign to the end for label
            label = tok[:m.start()].strip()
            # if we already buffered label pieces, prepend them
            if label_buf:
                label = (" ".join(label_buf + ([label] if label else []))).strip()
                label_buf = []
            # common artifacts like trailing ':' or trailing '£'
            label = label.rstrip(":£ ").strip()
            items.append({"label": label or "Charge", "amount": amount})
            expecting_bare_amount = False
            continue

        # no explicit '£' amount in this token
        if expecting_bare_amount and _number_token.match(tok):
            # this token is the amount that follows a label ending with '£'
            amount = Decimal(tok)
            label = " ".join(label_buf).rstrip(":£ ").strip() if label_buf else "Charge"
            items.append({"label": label or "Charge", "amount": amount})
            label_buf = []
            expecting_bare_amount = False
            continue

        # else: this token is part of a (possibly multi-part) label
        label_buf.append(tok)
        # if the token ends with a currency sign, expect a bare number next
        if tok.endswith("£"):
            expecting_bare_amount = True
        else:
            # also handle patterns like "VAT : £" (endswith('£') already catches most)
            if tok.endswith("£ 0") or tok.endswith("£ 0.00"):
                expecting_bare_amount = False

    # leftovers (label only)
    flush_unmatched_label()
    return items

def charges_total(items):
    """Sum the 'amount' fields from parse_charges() output."""
    total = Decimal("0.00")
    for c in items or []:
        amt = c.get("amount")
        if amt is not None:
            total += Decimal(str(amt))
    return total

def semi_br(s):
    """
    Replace semicolon-separated items with <br> lines.
    - Removes the semicolons
    - Trims whitespace
    - Skips empty fragments
    """
    if not s:
        return ""
    parts = [p.strip() for p in str(s).replace("\r", "").split(";")]
    parts = [p for p in parts if p]  # drop empties
    return Markup("<br>".join(escape(p) for p in parts))


_hyphen_join = re.compile(r"[;\n]+")  # split on ; or newline

# app/filters.py
import re
from markupsafe import Markup, escape

_split_lines = re.compile(r"[;\n]+", re.M)

import re
from markupsafe import Markup, escape

_split_lines = re.compile(r"[;\n]+", re.M)

def _strip_words(text, words):
    """Remove whole-word occurrences of any in `words` and collapse spaces."""
    if not text:
        return text
    for w in words:
        text = re.sub(rf"\b{re.escape(w)}\b", "", text, flags=re.I)
    # collapse multiple spaces and trim
    return re.sub(r"\s{2,}", " ", text).strip()

def tidy_address(s: str):
    """
    Clean address blocks:
    - split on ';' or newlines
    - merge hyphenated wraps (e.g., STRATFORD-UPON- + A VON -> STRATFORD-UPON-AVON)
    - lift UK/USA country fragments off surrounding lines and place the
      canonical country on the last line, with fragments removed.
    """
    if not s:
        return ""

    parts = [p.strip() for p in _split_lines.split(str(s).replace("\r", ""))]
    parts = [p for p in parts if p]

    # 1) merge hyphen-breaks (and the AVON artifact)
    merged = []
    i = 0
    while i < len(parts):
        cur = parts[i]
        if cur.endswith("-") and i + 1 < len(parts):
            nxt = parts[i + 1]
            if cur.upper().endswith("STRATFORD-UPON-") and nxt.upper().replace(" ", "") == "AVON":
                merged.append(cur + "AVON")
            else:
                merged.append(cur[:-1] + re.sub(r"\s+", "", nxt))
            i += 2
            continue
        cur = re.sub(r"(STRATFORD-UPON-)\s*A\s*VON\b", r"\1AVON", cur, flags=re.I)
        merged.append(cur)
        i += 1

    # 2) pull out country even if split across two lines
    out, country = [], None
    i = 0
    while i < len(merged):
        cur = merged[i]
        nxt = merged[i + 1] if i + 1 < len(merged) else ""

        combo = (cur + " " + nxt).strip()

        # patterns
        pat_uk  = re.compile(r"\bUnited\s+Kingdom\b", re.I)
        pat_usa = re.compile(r"\bUnited\s+States(?:\s+of)?(?:\s+America)?\b", re.I)

        took = False

        # UK: remove 'United'/'Kingdom' from cur/nxt and set canonical
        if pat_uk.search(combo):
            cur = _strip_words(cur, ["United", "Kingdom"])
            nxt2 = _strip_words(nxt, ["United", "Kingdom"])
            # if we removed from next, and it becomes empty, we'll skip it
            consume_next = (nxt != nxt2) and (nxt2 == "")
            nxt = nxt2
            country = "United Kingdom"
            took = True
            # append cleaned current line if still has content
            if cur:
                out.append(cur)
            if consume_next:
                i += 2
            else:
                i += 1
        # USA: remove 'United','States','of','America' and set canonical
        elif pat_usa.search(combo):
            words = ["United", "States", "of", "America"]
            cur2 = _strip_words(cur, words)
            nxt2 = _strip_words(nxt, words)
            consume_next = (nxt != nxt2) and (nxt2 == "")
            cur, nxt = cur2, nxt2
            country = "United States of America"
            took = True
            if cur:
                out.append(cur)
            if consume_next:
                i += 2
            else:
                i += 1

        if not took:
            if cur:
                out.append(cur)
            i += 1

    # drop empties and append canonical country if found
    out = [x for x in out if x]
    if country:
        out.append(country)

    return Markup("<br>".join(escape(x) for x in out))


def norm_ref(s):
    if not s: return ""
    # strip, collapse internal whitespace, lowercase
    return " ".join(str(s).strip().split()).lower()



def register_filters(app):
    app.jinja_env.filters["nz"] = nz
    app.jinja_env.filters["blank_if_zero"] = blank_if_zero
    app.jinja_env.filters["fmt"] = fmt_num_always
    app.jinja_env.filters["iif"] = iif
    app.jinja_env.filters["fmt_date"] = fmt_date
    app.jinja_env.filters["flag"] = flag
    app.jinja_env.filters["trim2"] = trim2
    app.jinja_env.filters["country_name"] = country_name
    app.jinja_env.filters.update({"nl2br": nl2br})
    app.jinja_env.filters.update({
        "parse_charges": parse_charges,
        "charges_total": charges_total,
        "semi_br": semi_br,
        "tidy_address": tidy_address, 
        "norm_ref": norm_ref,        
        # ...your other filters...
    })