rehan953 commited on
Commit
df459b3
Β·
verified Β·
1 Parent(s): ed040f2

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +186 -0
app.py CHANGED
@@ -1886,6 +1886,191 @@ def _normalize_checks_paid_table(grid):
1886
  return new_rows
1887
 
1888
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1889
  def normalize_html_tables(text: str) -> str:
1890
  """
1891
  For every <table>...</table>:
@@ -1978,6 +2163,7 @@ def stabilize_tables_and_text(page_md: str) -> str:
1978
  out_blocks.append(b)
1979
 
1980
  stabilized = "\n\n".join(out_blocks)
 
1981
  stabilized = normalize_html_tables(stabilized)
1982
  return close_unclosed_html(stabilized)
1983
 
 
1886
  return new_rows
1887
 
1888
 
1889
+ # ── Johnson Bank plain-text section converter ─────────────────────────────
1890
+ # Johnson Bank formats Deposits, Withdrawals, Checks, and Daily Account Balance
1891
+ # as plain text columns (no HTML tables). This converter detects those sections
1892
+ # and emits proper <table> HTML.
1893
+ #
1894
+ # SAFETY GUARDS β€” all four must hold before any conversion fires:
1895
+ # 1. Line matches exact section-header pattern
1896
+ # 2. Next non-blank line matches exact column-header pattern
1897
+ # 3. First data row uses MM-DD date format (Johnson Bank's unique date separator)
1898
+ # 4. The candidate section contains NO existing <table> tags
1899
+ # Guard 3 is the key discriminator: all other supported banks use MM/DD.
1900
+
1901
+ _JB_SECTION_HDR = re.compile(
1902
+ r"^(Deposits?|Withdrawals?|Checks?(?:\s+Paid)?|Daily\s+Account\s+Balance)"
1903
+ r"\s*(?:\(cont\.?\))?\s*$",
1904
+ re.IGNORECASE,
1905
+ )
1906
+ _JB_COL_HDR = re.compile(
1907
+ r"^Date\s+(Description|Number)\s+Amount\s*$"
1908
+ r"|^Date\s+(?:Number\s+)?(?:Balance|Amount)\s*$",
1909
+ re.IGNORECASE,
1910
+ )
1911
+ # MM-DD date (dash separator) β€” Johnson Bank's unique format
1912
+ _JB_DATE_DD = re.compile(r"^\d{2}-\d{2}\s+")
1913
+ _JB_TXN_ROW = re.compile(r"^(\d{2}-\d{2})\s+(.+?)\s+(-?\d{1,3}(?:,\d{3})*\.\d{2})\s*$")
1914
+ _JB_BAL_ROW = re.compile(r"^(\d{2}-\d{2})\s+(\d{1,3}(?:,\d{3})*\.\d{2})\s*$")
1915
+
1916
+
1917
+ def _jb_rows_to_html(header_cols, rows):
1918
+ th = "".join("<th>" + h + "</th>" for h in header_cols)
1919
+ parts = ["<table>", "<tr>" + th + "</tr>"]
1920
+ for row in rows:
1921
+ td = "".join("<td>" + str(row.get(h, "")) + "</td>" for h in header_cols)
1922
+ parts.append("<tr>" + td + "</tr>")
1923
+ parts.append("</table>")
1924
+ return "\n".join(parts)
1925
+
1926
+
1927
+ def _jb_section_has_table(lines, start, end):
1928
+ """Return True if any line in lines[start:end] contains a <table tag."""
1929
+ return any("<table" in l.lower() for l in lines[start:end])
1930
+
1931
+
1932
+ def _find_first_data_row(lines, start):
1933
+ """Return index of first non-blank line after start, or None."""
1934
+ for i in range(start, min(start + 20, len(lines))):
1935
+ if lines[i].strip():
1936
+ return i
1937
+ return None
1938
+
1939
+
1940
+ def convert_plaintext_bank_sections(text: str) -> str:
1941
+ """
1942
+ Convert Johnson Bank plain-text transaction sections to HTML tables.
1943
+ All four safety guards must pass before conversion fires.
1944
+ Safe no-op for all other banks.
1945
+ """
1946
+ # Quick exit: if the page already has lots of tables, skip entirely
1947
+ # (handles pages that are already properly formatted)
1948
+ lines = text.splitlines()
1949
+ out = []
1950
+ i = 0
1951
+
1952
+ while i < len(lines):
1953
+ line = lines[i].strip()
1954
+
1955
+ # Guard 1: section header
1956
+ if not _JB_SECTION_HDR.match(line):
1957
+ out.append(lines[i])
1958
+ i += 1
1959
+ continue
1960
+
1961
+ section_title = line
1962
+ j = i + 1
1963
+
1964
+ # Skip blank lines to find column header
1965
+ while j < len(lines) and not lines[j].strip():
1966
+ j += 1
1967
+
1968
+ # Guard 2: column header
1969
+ if j >= len(lines) or not _JB_COL_HDR.match(lines[j].strip()):
1970
+ out.append(lines[i])
1971
+ i += 1
1972
+ continue
1973
+
1974
+ col_hdr_line = lines[j].strip()
1975
+ data_start = j + 1
1976
+
1977
+ # Find first non-blank data line
1978
+ first_data_idx = _find_first_data_row(lines, data_start)
1979
+
1980
+ # Guard 3: first data row must use MM-DD date format
1981
+ if first_data_idx is None or not _JB_DATE_DD.match(lines[first_data_idx].strip()):
1982
+ out.append(lines[i])
1983
+ i += 1
1984
+ continue
1985
+
1986
+ # Find section end (next section header, page separator, or end of text)
1987
+ section_end = len(lines)
1988
+ for k in range(data_start, len(lines)):
1989
+ l = lines[k].strip()
1990
+ if (l != section_title and _JB_SECTION_HDR.match(l)
1991
+ or l.startswith("---page-separator")
1992
+ or l.startswith("Page:")):
1993
+ section_end = k
1994
+ break
1995
+
1996
+ # Guard 4: no existing <table> tags in this section
1997
+ if _jb_section_has_table(lines, i, section_end):
1998
+ out.append(lines[i])
1999
+ i += 1
2000
+ continue
2001
+
2002
+ # All guards passed β€” parse and convert
2003
+ parts_upper = col_hdr_line.upper().split()
2004
+ if len(parts_upper) == 2 and parts_upper[1] == "BALANCE":
2005
+ header_cols = ["Date", "Balance"]
2006
+ elif "NUMBER" in parts_upper:
2007
+ header_cols = ["Date", "Number", "Amount"]
2008
+ else:
2009
+ header_cols = ["Date", "Description", "Amount"]
2010
+
2011
+ out.append(section_title)
2012
+ rows = []
2013
+ current = None
2014
+ k = data_start
2015
+
2016
+ while k < section_end:
2017
+ raw = lines[k]
2018
+ row_line = raw.strip()
2019
+
2020
+ if not row_line:
2021
+ k += 1
2022
+ continue
2023
+
2024
+ if (_JB_SECTION_HDR.match(row_line)
2025
+ or row_line.startswith("---page-separator")
2026
+ or row_line.startswith("Page:")):
2027
+ break
2028
+
2029
+ # Balance-only row
2030
+ mb = _JB_BAL_ROW.match(row_line)
2031
+ if mb and header_cols == ["Date", "Balance"]:
2032
+ if current:
2033
+ rows.append(current)
2034
+ current = {"Date": mb.group(1), "Balance": mb.group(2)}
2035
+ k += 1
2036
+ continue
2037
+
2038
+ # Full transaction row
2039
+ mt = _JB_TXN_ROW.match(row_line)
2040
+ if mt:
2041
+ if current:
2042
+ rows.append(current)
2043
+ desc_key = "Description" if "Description" in header_cols else "Number"
2044
+ current = {
2045
+ "Date": mt.group(1),
2046
+ desc_key: mt.group(2).strip(),
2047
+ "Amount": mt.group(3),
2048
+ }
2049
+ k += 1
2050
+ continue
2051
+
2052
+ # Continuation line
2053
+ if current and row_line:
2054
+ desc_key = "Description" if "Description" in header_cols else "Number"
2055
+ if desc_key in current:
2056
+ current[desc_key] += " " + row_line
2057
+ k += 1
2058
+ continue
2059
+
2060
+ k += 1
2061
+
2062
+ if current:
2063
+ rows.append(current)
2064
+
2065
+ if rows:
2066
+ out.append(_jb_rows_to_html(header_cols, rows))
2067
+
2068
+ i = section_end # jump past the whole section
2069
+ continue
2070
+
2071
+ return "\n".join(out)
2072
+
2073
+
2074
  def normalize_html_tables(text: str) -> str:
2075
  """
2076
  For every <table>...</table>:
 
2163
  out_blocks.append(b)
2164
 
2165
  stabilized = "\n\n".join(out_blocks)
2166
+ stabilized = convert_plaintext_bank_sections(stabilized)
2167
  stabilized = normalize_html_tables(stabilized)
2168
  return close_unclosed_html(stabilized)
2169