From cab22dc406628bde9d7d6974ae3b295e399fbd18 Mon Sep 17 00:00:00 2001 From: "youchang.kim" Date: Mon, 2 Nov 2026 23:21:27 +0900 Subject: [PATCH] table: group cell text into lines by the font-metric line top (#5198) find_tables() sets small glyph heights so that every character on a line shares one top, but that only takes effect while quad corrections are on. With corrections off, which is the state after importing pymupdf4llm and always the state on the layout path, each character keeps its exact glyph box. The "." of "0.74" then starts about 7 pt below the digits, the pdfplumber-derived word extractor puts it on a line of its own, and Table.extract() returns "0 74\n.". Space characters with empty glyph boxes land in another line cluster the same way, so words lose their spaces at small font sizes. On rotated pages the same boxes scramble the word order and split "12.5" into "12 . 5". make_chars() now stores a "line_top" for every character: the top of the text line computed from the character's baseline and the span's normalised ascender, which is where the quad correction with small glyph heights puts the top of the character box. For rotated lines, whose text lines stack along x, it stores the same distance along x as "line_x". WordExtractor groups characters and words into lines by these values instead of the glyph box edges, a word carrying the smallest value of its characters as it carries the smallest top, and extract_text() clusters and orders words by them too. With corrections on the values equal the existing box edges, so output stays the same there, except for glyphs that lie entirely below the baseline (underscores): the correction boxes those as upside-down text, below the line, and they used to come out on a line of their own ("Prem Written\n_"); they now stay in their word ("Prem_Written"). Cell membership and the glyph boxes themselves are untouched; the text strategy and the layout=True text map consume the same words and see merged numbers with corrections off. The layout-branch expectations of seven tests in tests/test_tables.py recorded the split output. They are regenerated from the output of this change, with and without pymupdf4llm installed, and now agree with the non-layout branches of the same tests. test_4017 had two layout sub-branches, for a process with and without pymupdf4llm; since #5205 find_tables() turns corrections off before get_layout() in both cases and they produce the same four tables, so they are now one expectation. tests/test_5198.py covers the word extractor with hand-made characters, the decimal point with corrections on and off for all four page rotations and on the layout path, a raised footnote marker, and spaces between small words. --- src/table.py | 75 +++++++++-- tests/test_5198.py | 168 +++++++++++++++++++++++ tests/test_tables.py | 309 +++++++++++++++++-------------------------- 3 files changed, 353 insertions(+), 199 deletions(-) create mode 100644 tests/test_5198.py diff --git a/src/table.py b/src/table.py index f20d777c2..b62d8facd 100644 --- a/src/table.py +++ b/src/table.py @@ -700,6 +700,30 @@ def to_textmap( return TextMap(_textmap) +def _line_top(obj): + """Vertical position used to group chars and words into text lines. + + make_chars() stores "line_top" for every char: the top of the text line + computed from the char's baseline and font metrics, i.e. where the quad + correction with small glyph heights would put the top of the char box. It + is the same for every char on a line whether or not quad corrections are + on. The bbox top ("doctop") is used for objects that do not carry it. + """ + return obj.get("line_top", obj["doctop"]) + + +def _line_x0(obj): + """Left edge used to group rotated (non-upright) chars and words into lines: + the metric-derived "line_x0" stored by make_chars() for vertical lines, else + the bbox left edge ("x0").""" + return obj.get("line_x0", obj["x0"]) + + +def _line_x1(obj): + """Right edge used to order the lines of a rotated cell, same source.""" + return obj.get("line_x1", obj["x1"]) + + class WordExtractor: def __init__( self, @@ -732,6 +756,9 @@ def __init__( def merge_chars(self, ordered_chars: list): x0, top, x1, bottom = objects_to_bbox(ordered_chars) + line_tops = [c["line_top"] for c in ordered_chars if "line_top" in c] + line_x0s = [c["line_x0"] for c in ordered_chars if "line_x0" in c] + line_x1s = [c["line_x1"] for c in ordered_chars if "line_x1" in c] doctop_adj = ordered_chars[0]["doctop"] - ordered_chars[0]["top"] upright = ordered_chars[0]["upright"] direction = 1 if (self.horizontal_ltr if upright else self.vertical_ttb) else -1 @@ -765,6 +792,12 @@ def merge_chars(self, ordered_chars: list): for key in self.extra_attrs: word[key] = ordered_chars[0][key] + if line_tops: # like the word top, the smallest of its chars + word["line_top"] = min(line_tops) + if line_x0s: # like the word's x0 and x1 + word["line_x0"] = min(line_x0s) + word["line_x1"] = max(line_x1s) + return word def char_begins_new_word( @@ -809,8 +842,8 @@ def char_begins_new_word( if curr_char["upright"]: x = self.x_tolerance y = self.y_tolerance - ay = prev_char["top"] - cy = curr_char["top"] + ay = _line_top(prev_char) + cy = _line_top(curr_char) if self.horizontal_ltr: ax = prev_char["x0"] bx = prev_char["x1"] @@ -823,8 +856,8 @@ def char_begins_new_word( else: x = self.y_tolerance y = self.x_tolerance - ay = prev_char["x0"] - cy = curr_char["x0"] + ay = _line_x0(prev_char) + cy = _line_x0(curr_char) if self.vertical_ttb: ax = prev_char["top"] bx = prev_char["bottom"] @@ -879,11 +912,11 @@ def upright_key(x) -> int: for upright_cluster in cluster_objects(list(chars), upright_key, 0): upright = upright_cluster[0]["upright"] - cluster_key = "doctop" if upright else "x0" + cluster_key = _line_top if upright else _line_x0 # Cluster by line subclusters = cluster_objects( - upright_cluster, itemgetter(cluster_key), self.y_tolerance + upright_cluster, cluster_key, self.y_tolerance ) for sc in subclusters: @@ -956,13 +989,13 @@ def extract_text(chars: list, **kwargs) -> str: rotation = 0 if rotation == 90: - words.sort(key=lambda w: (w["x1"], -w["top"])) + words.sort(key=lambda w: (_line_x1(w), -w["top"])) lines = " ".join([w["text"] for w in words]) elif rotation == 270: - words.sort(key=lambda w: (-w["x1"], w["top"])) + words.sort(key=lambda w: (-_line_x1(w), w["top"])) lines = " ".join([w["text"] for w in words]) else: - lines = cluster_objects(words, itemgetter("doctop"), y_tolerance) + lines = cluster_objects(words, _line_top, y_tolerance) lines = "\n".join(" ".join(word["text"] for word in line) for line in lines) if rotation == 180: # needs extra treatment lines = "".join([(c if c != "\n" else " ") for c in reversed(lines)]) @@ -2415,6 +2448,21 @@ def make_chars(page, clip=None): for span in sorted(line["spans"], key=lambda s: s["bbox"][0]): fontname = span["font"] fontsize = span["size"] + # Line top relative to the baseline: the normalised ascender + # times the font size, which is what the quad correction with + # small glyph heights uses for the top of every char box. + # With quad corrections off each char keeps its exact glyph + # box instead, so a "." starts far below the digits around + # it; grouping by this value keeps such chars on their line. + # Vertical lines (page rotated by 90 or 270 degrees) stack along + # x; for them the two box edges along x are derived the same way + # ("line_x0", "line_x1"). The normalised ascender and descender + # add up to the font size, so the descender side is the rest. + asc = span.get("ascender", 0.8) + dsc = span.get("descender", -0.2) + if asc < 1e-3: # glyphless font, same fallback as the quad correction + asc, dsc = 0.9, -0.1 + line_ascent = fontsize * (asc / (asc - dsc) if asc - dsc > 0 else 0.8) span_bold = bool( span["flags"] & pymupdf.TEXT_FONT_BOLD or span["char_flags"] & 8 ) @@ -2430,6 +2478,7 @@ def make_chars(page, clip=None): "adv": bbox.x1 - bbox.x0 if upright else bbox.y1 - bbox.y0, "bottom": bbox.y1, "doctop": bbox.y0 + doctop_base, + "line_top": char["origin"][1] - line_ascent + doctop_base, "fontname": fontname, "height": bbox.y1 - bbox.y0, "matrix": tuple(matrix), @@ -2451,6 +2500,14 @@ def make_chars(page, clip=None): "y0": bbox_ctm.y0, "y1": bbox_ctm.y1, } + if not upright and ldir[0] == 0: + ox = char["origin"][0] + if ldir[1] > 0: # glyph top points to +x + char_dict["line_x0"] = ox - (fontsize - line_ascent) + char_dict["line_x1"] = ox + line_ascent + else: # glyph top points to -x + char_dict["line_x0"] = ox - line_ascent + char_dict["line_x1"] = ox + (fontsize - line_ascent) chars.append(char_dict) return TEXTPAGE diff --git a/tests/test_5198.py b/tests/test_5198.py new file mode 100644 index 000000000..fba452683 --- /dev/null +++ b/tests/test_5198.py @@ -0,0 +1,168 @@ +"""Table.extract() keeps "0.74" on one line whatever the quad-corrections toggle says. + +Issue #5198: with quad corrections off, which is the state after importing +pymupdf4llm and always the state on the layout path, every character keeps its +exact glyph box. The "." of "0.74" then starts about 7 pt below the digits and +extract_text() grouped it onto a line of its own, so the cell read "0 74\\n.". +Spaces between words were lost the same way when the space character landed in +another line cluster. Cell text is now grouped by the top of the text line +derived from each char's baseline and font metrics, which is the same for every +char on a line in both toggle states. +""" +import pymupdf +import pytest +from pymupdf.table import extract_text, extract_words + +GRID = [ + ["P for interaction", "Cooking fuel type"], + ["0.74", "12.5 mg"], +] + + +def _bordered_grid(page, bbox, texts, fontsize=11): + x0, y0, x1, y1 = bbox + rows, cols = len(texts), len(texts[0]) + rh, cw = (y1 - y0) / rows, (x1 - x0) / cols + for r in range(rows + 1): + page.draw_line((x0, y0 + r * rh), (x1, y0 + r * rh)) + for c in range(cols + 1): + page.draw_line((x0 + c * cw, y0), (x0 + c * cw, y1)) + for r, values in enumerate(texts): + for c, value in enumerate(values): + page.insert_text( + (x0 + c * cw + 8, y0 + (r + 1) * rh - 12), value, fontsize=fontsize + ) + + +def _rotated(expected, rotation): + for _ in range(rotation // 90): + expected = [list(row) for row in zip(*reversed(expected))] + return expected + + +class _quad_corrections_off: + """Set the toggle as an import of pymupdf4llm does, restore afterwards.""" + + def __init__(self, off): + self.off = off + + def __enter__(self): + self.old = pymupdf.TOOLS.unset_quad_corrections() + pymupdf.TOOLS.unset_quad_corrections(self.off) + + def __exit__(self, *exc): + pymupdf.TOOLS.unset_quad_corrections(self.old) + + +# -------------------------------------------------------------------------- +# Unit tests on the word extractor with hand-made char dicts (no PDF, no toggle) +# -------------------------------------------------------------------------- +def _char(text, x0, top, bottom, line_top=None, width=6): + d = { + "text": text, + "x0": x0, + "x1": x0 + width, + "top": top, + "bottom": bottom, + "doctop": top, + "upright": True, + "matrix": (1, 0, 0, 1, x0, bottom), + } + if line_top is not None: + d["line_top"] = line_top + return d + + +def test_line_top_groups_a_low_glyph_with_its_line(): + """Glyph-tight boxes: the "." starts 6.5 pt below the digits. Grouped by + the shared line top it stays in the number; grouped by the glyph top (the + fallback for chars without line_top) it does not.""" + tops = {"0": 100, ".": 106.5, "7": 100, "4": 100} + with_line_top = [ + _char(t, 10 + 6 * i, tops[t], 108, line_top=100) for i, t in enumerate("0.74") + ] + assert extract_text(with_line_top) == "0.74" + legacy = [_char(t, 10 + 6 * i, tops[t], 108) for i, t in enumerate("0.74")] + assert extract_text(legacy) == "0 74\n." + + +def test_word_line_top_is_its_smallest_char_line_top(): + """With corrections on every char's line_top equals its box top, and a + word's line top must then equal its bbox top (the smallest char top) so + that line clustering of words is unchanged: a word mixing a tall first + char with lower ones still shares the line of a word at the tall top.""" + word = [_char(t, 10 + 6 * i, top, top + 8, line_top=top) + for i, (t, top) in enumerate(zip("ABCCC", (100, 102, 104, 104, 104)))] + other = [_char("X", 60, 100, 108, line_top=100)] + words = extract_words(word + other) + assert [w["text"] for w in words] == ["ABCCC", "X"] + assert words[0]["line_top"] == 100 == words[0]["top"] + assert extract_text(word + other) == "ABCCC X" + + +# -------------------------------------------------------------------------- +# Integration tests through find_tables() +# -------------------------------------------------------------------------- +@pytest.mark.parametrize("quad_off", (False, True)) +@pytest.mark.parametrize("rotation", (0, 90, 180, 270)) +def test_decimal_point_line_based(rotation, quad_off): + with _quad_corrections_off(quad_off), pymupdf.open() as doc: + page = doc.new_page() + _bordered_grid(page, (72, 72, 472, 152), GRID) + page.set_rotation(rotation) + tables = page.find_tables(use_layout=False).tables + assert len(tables) == 1 + assert tables[0].extract() == _rotated(GRID, rotation) + # find_tables() leaves the toggle as it found it + assert pymupdf.TOOLS.unset_quad_corrections() == quad_off + + +@pytest.mark.parametrize("quad_off", (False, True)) +def test_decimal_point_layout_path(quad_off): + if not pymupdf._layout_available(): + pytest.skip("pymupdf-layout not installed") + with _quad_corrections_off(quad_off), pymupdf.open() as doc: + page = doc.new_page() + _bordered_grid(page, (72, 72, 472, 152), GRID) + tables = page.find_tables().tables + assert len(tables) == 1 + # The layout path may add an empty column (a separate issue); each + # row's non-empty cells must still read as written. + rows = [[c for c in row if c] for row in tables[0].extract()] + assert rows == GRID + + +@pytest.mark.parametrize("quad_off", (False, True)) +def test_superscript_stays_on_its_line(quad_off): + """A raised, smaller footnote marker belongs to the line it annotates. + + Grouping by the baseline alone would put "a" (raised 3.5 pt) on a line of + its own; the font-metric line top keeps it with "Total", as the corrected + boxes did. + """ + with _quad_corrections_off(quad_off), pymupdf.open() as doc: + page = doc.new_page() + x0, y0, x1, y1, xm = 72, 380, 372, 420, 222 + for p1, p2 in (((x0, y0), (x1, y0)), ((x0, y1), (x1, y1)), + ((x0, y0), (x0, y1)), ((xm, y0), (xm, y1)), ((x1, y0), (x1, y1))): + page.draw_line(p1, p2) + page.insert_text((80, 405), "Total", fontsize=11) + page.insert_text((80 + pymupdf.get_text_length("Total", fontsize=11) + 1, 401.5), "a", fontsize=7) + page.insert_text((230, 405), "12.5", fontsize=11) + page.insert_text((230 + pymupdf.get_text_length("12.5", fontsize=11) + 1, 401.5), "*", fontsize=7) + tables = page.find_tables(use_layout=False).tables + assert len(tables) == 1 + assert tables[0].extract() == [["Totala", "12.5*"]] + + +@pytest.mark.parametrize("quad_off", (False, True)) +def test_spaces_between_words(quad_off): + """Small text: word gaps are under the 3 pt x tolerance, so the space + characters themselves must stay on the line to separate the words.""" + texts = [["Class A/B Overcollateralization", "Event of Default"]] + with _quad_corrections_off(quad_off), pymupdf.open() as doc: + page = doc.new_page() + _bordered_grid(page, (72, 72, 472, 100), texts, fontsize=8) + tables = page.find_tables(use_layout=False).tables + assert len(tables) == 1 + assert tables[0].extract() == texts diff --git a/tests/test_tables.py b/tests/test_tables.py index df2344b15..490f59a4f 100644 --- a/tests/test_tables.py +++ b/tests/test_tables.py @@ -106,24 +106,24 @@ def test_table1(): expected = [ [ [b'\xe5\xb9\xb4\xe4\xbb\xbd'.decode(), b'\xe4\xb8\xbb\xe4\xbd\x93\xe4\xbf\xa1\xe7\x94\xa8\n\xe8\xaf\x84\xe7\xba\xa7'.decode(), b'\xe6\xb6\xb5\xe4\xb9\x89'.decode(), b'\xe8\xaf\x84\xe7\xba\xa7\xe6\x9c\xba\xe6\x9e\x84'.decode(), b'\xe8\xaf\x84\xe7\xba\xa7\xe5\xb1\x95\xe6\x9c\x9b'.decode(), ], - ['2023', 'AA+', b'\xe5\x81\xbf\xe8\xbf\x98\xe5\x80\xba\xe5\x8a\xa1\xe7\x9a\x84\xe8\x83\xbd\xe5\x8a\x9b\xe5\xbe\x88\xe5\xbc\xba \xe5\x8f\x97\xe4\xb8\x8d\xe5\x88\xa9\xe7\xbb\x8f\xe6\xb5\x8e\n\xef\xbc\x8c\n\xe7\x8e\xaf\xe5\xa2\x83\xe7\x9a\x84\xe5\xbd\xb1\xe5\x93\x8d\xe4\xb8\x8d\xe5\xa4\xa7 \xe8\xbf\x9d\xe7\xba\xa6\xe9\xa3\x8e\xe9\x99\xa9\xe5\xbe\x88\xe4\xbd\x8e\n\xef\xbc\x8c'.decode(), b'\xe4\xb8\xad\xe8\xaf\x9a\xe4\xbf\xa1\xe5\x9b\xbd\xe9\x99\x85\xe4\xbf\xa1\n\xe7\x94\xa8\xe8\xaf\x84\xe7\xba\xa7\xe6\x9c\x89\xe9\x99\x90\xe8\xb4\xa3\n\xe4\xbb\xbb\xe5\x85\xac\xe5\x8f\xb8 \xe8\x81\x94\xe5\x90\x88\xe8\xb5\x84\n\xe3\x80\x81\n\xe4\xbf\xa1\xe8\xaf\x84\xe4\xbc\xb0\xe6\x9c\x89\xe9\x99\x90\xe5\x85\xac\n\xe5\x8f\xb8'.decode(), b'\xe7\xa8\xb3\xe5\xae\x9a'.decode(), ], - ['2022', 'AA+', b'\xe5\x81\xbf\xe8\xbf\x98\xe5\x80\xba\xe5\x8a\xa1\xe7\x9a\x84\xe8\x83\xbd\xe5\x8a\x9b\xe5\xbe\x88\xe5\xbc\xba \xe5\x8f\x97\xe4\xb8\x8d\xe5\x88\xa9\xe7\xbb\x8f\xe6\xb5\x8e\n\xef\xbc\x8c\n\xe7\x8e\xaf\xe5\xa2\x83\xe7\x9a\x84\xe5\xbd\xb1\xe5\x93\x8d\xe4\xb8\x8d\xe5\xa4\xa7 \xe8\xbf\x9d\xe7\xba\xa6\xe9\xa3\x8e\xe9\x99\xa9\xe5\xbe\x88\xe4\xbd\x8e\n\xef\xbc\x8c'.decode(), b'\xe4\xb8\xad\xe8\xaf\x9a\xe4\xbf\xa1\xe5\x9b\xbd\xe9\x99\x85\xe4\xbf\xa1\n\xe7\x94\xa8\xe8\xaf\x84\xe7\xba\xa7\xe6\x9c\x89\xe9\x99\x90\xe8\xb4\xa3\n\xe4\xbb\xbb\xe5\x85\xac\xe5\x8f\xb8 \xe8\x81\x94\xe5\x90\x88\xe8\xb5\x84\n\xe3\x80\x81\n\xe4\xbf\xa1\xe8\xaf\x84\xe4\xbc\xb0\xe6\x9c\x89\xe9\x99\x90\xe5\x85\xac\n\xe5\x8f\xb8'.decode(), b'\xe7\xa8\xb3\xe5\xae\x9a'.decode(), ], - ['2021', 'AA+', b'\xe5\x81\xbf\xe8\xbf\x98\xe5\x80\xba\xe5\x8a\xa1\xe7\x9a\x84\xe8\x83\xbd\xe5\x8a\x9b\xe5\xbe\x88\xe5\xbc\xba \xe5\x8f\x97\xe4\xb8\x8d\xe5\x88\xa9\xe7\xbb\x8f\xe6\xb5\x8e\n\xef\xbc\x8c\n\xe7\x8e\xaf\xe5\xa2\x83\xe7\x9a\x84\xe5\xbd\xb1\xe5\x93\x8d\xe4\xb8\x8d\xe5\xa4\xa7 \xe8\xbf\x9d\xe7\xba\xa6\xe9\xa3\x8e\xe9\x99\xa9\xe5\xbe\x88\xe4\xbd\x8e\n\xef\xbc\x8c'.decode(), b'\xe8\x81\x94\xe5\x90\x88\xe8\xb5\x84\xe4\xbf\xa1\xe8\xaf\x84\xe4\xbc\xb0\n\xe6\x9c\x89\xe9\x99\x90\xe5\x85\xac\xe5\x8f\xb8'.decode(), b'\xe7\xa8\xb3\xe5\xae\x9a'.decode(), ], - ['2020', 'AA+', b'\xe5\x81\xbf\xe8\xbf\x98\xe5\x80\xba\xe5\x8a\xa1\xe7\x9a\x84\xe8\x83\xbd\xe5\x8a\x9b\xe5\xbe\x88\xe5\xbc\xba \xe5\x8f\x97\xe4\xb8\x8d\xe5\x88\xa9\xe7\xbb\x8f\xe6\xb5\x8e\n\xef\xbc\x8c\n\xe7\x8e\xaf\xe5\xa2\x83\xe7\x9a\x84\xe5\xbd\xb1\xe5\x93\x8d\xe4\xb8\x8d\xe5\xa4\xa7 \xe8\xbf\x9d\xe7\xba\xa6\xe9\xa3\x8e\xe9\x99\xa9\xe5\xbe\x88\xe4\xbd\x8e\n\xef\xbc\x8c'.decode(), b'\xe8\x81\x94\xe5\x90\x88\xe8\xb5\x84\xe4\xbf\xa1\xe8\xaf\x84\xe4\xbc\xb0\n\xe6\x9c\x89\xe9\x99\x90\xe5\x85\xac\xe5\x8f\xb8'.decode(), b'\xe7\xa8\xb3\xe5\xae\x9a'.decode(), ], + ['2023', 'AA+', b'\xe5\x81\xbf\xe8\xbf\x98\xe5\x80\xba\xe5\x8a\xa1\xe7\x9a\x84\xe8\x83\xbd\xe5\x8a\x9b\xe5\xbe\x88\xe5\xbc\xba\xef\xbc\x8c\xe5\x8f\x97\xe4\xb8\x8d\xe5\x88\xa9\xe7\xbb\x8f\xe6\xb5\x8e\n\xe7\x8e\xaf\xe5\xa2\x83\xe7\x9a\x84\xe5\xbd\xb1\xe5\x93\x8d\xe4\xb8\x8d\xe5\xa4\xa7\xef\xbc\x8c\xe8\xbf\x9d\xe7\xba\xa6\xe9\xa3\x8e\xe9\x99\xa9\xe5\xbe\x88\xe4\xbd\x8e'.decode(), b'\xe4\xb8\xad\xe8\xaf\x9a\xe4\xbf\xa1\xe5\x9b\xbd\xe9\x99\x85\xe4\xbf\xa1\n\xe7\x94\xa8\xe8\xaf\x84\xe7\xba\xa7\xe6\x9c\x89\xe9\x99\x90\xe8\xb4\xa3\n\xe4\xbb\xbb\xe5\x85\xac\xe5\x8f\xb8\xe3\x80\x81\xe8\x81\x94\xe5\x90\x88\xe8\xb5\x84\n\xe4\xbf\xa1\xe8\xaf\x84\xe4\xbc\xb0\xe6\x9c\x89\xe9\x99\x90\xe5\x85\xac\n\xe5\x8f\xb8'.decode(), b'\xe7\xa8\xb3\xe5\xae\x9a'.decode(), ], + ['2022', 'AA+', b'\xe5\x81\xbf\xe8\xbf\x98\xe5\x80\xba\xe5\x8a\xa1\xe7\x9a\x84\xe8\x83\xbd\xe5\x8a\x9b\xe5\xbe\x88\xe5\xbc\xba\xef\xbc\x8c\xe5\x8f\x97\xe4\xb8\x8d\xe5\x88\xa9\xe7\xbb\x8f\xe6\xb5\x8e\n\xe7\x8e\xaf\xe5\xa2\x83\xe7\x9a\x84\xe5\xbd\xb1\xe5\x93\x8d\xe4\xb8\x8d\xe5\xa4\xa7\xef\xbc\x8c\xe8\xbf\x9d\xe7\xba\xa6\xe9\xa3\x8e\xe9\x99\xa9\xe5\xbe\x88\xe4\xbd\x8e'.decode(), b'\xe4\xb8\xad\xe8\xaf\x9a\xe4\xbf\xa1\xe5\x9b\xbd\xe9\x99\x85\xe4\xbf\xa1\n\xe7\x94\xa8\xe8\xaf\x84\xe7\xba\xa7\xe6\x9c\x89\xe9\x99\x90\xe8\xb4\xa3\n\xe4\xbb\xbb\xe5\x85\xac\xe5\x8f\xb8\xe3\x80\x81\xe8\x81\x94\xe5\x90\x88\xe8\xb5\x84\n\xe4\xbf\xa1\xe8\xaf\x84\xe4\xbc\xb0\xe6\x9c\x89\xe9\x99\x90\xe5\x85\xac\n\xe5\x8f\xb8'.decode(), b'\xe7\xa8\xb3\xe5\xae\x9a'.decode(), ], + ['2021', 'AA+', b'\xe5\x81\xbf\xe8\xbf\x98\xe5\x80\xba\xe5\x8a\xa1\xe7\x9a\x84\xe8\x83\xbd\xe5\x8a\x9b\xe5\xbe\x88\xe5\xbc\xba\xef\xbc\x8c\xe5\x8f\x97\xe4\xb8\x8d\xe5\x88\xa9\xe7\xbb\x8f\xe6\xb5\x8e\n\xe7\x8e\xaf\xe5\xa2\x83\xe7\x9a\x84\xe5\xbd\xb1\xe5\x93\x8d\xe4\xb8\x8d\xe5\xa4\xa7\xef\xbc\x8c\xe8\xbf\x9d\xe7\xba\xa6\xe9\xa3\x8e\xe9\x99\xa9\xe5\xbe\x88\xe4\xbd\x8e'.decode(), b'\xe8\x81\x94\xe5\x90\x88\xe8\xb5\x84\xe4\xbf\xa1\xe8\xaf\x84\xe4\xbc\xb0\n\xe6\x9c\x89\xe9\x99\x90\xe5\x85\xac\xe5\x8f\xb8'.decode(), b'\xe7\xa8\xb3\xe5\xae\x9a'.decode(), ], + ['2020', 'AA+', b'\xe5\x81\xbf\xe8\xbf\x98\xe5\x80\xba\xe5\x8a\xa1\xe7\x9a\x84\xe8\x83\xbd\xe5\x8a\x9b\xe5\xbe\x88\xe5\xbc\xba\xef\xbc\x8c\xe5\x8f\x97\xe4\xb8\x8d\xe5\x88\xa9\xe7\xbb\x8f\xe6\xb5\x8e\n\xe7\x8e\xaf\xe5\xa2\x83\xe7\x9a\x84\xe5\xbd\xb1\xe5\x93\x8d\xe4\xb8\x8d\xe5\xa4\xa7\xef\xbc\x8c\xe8\xbf\x9d\xe7\xba\xa6\xe9\xa3\x8e\xe9\x99\xa9\xe5\xbe\x88\xe4\xbd\x8e'.decode(), b'\xe8\x81\x94\xe5\x90\x88\xe8\xb5\x84\xe4\xbf\xa1\xe8\xaf\x84\xe4\xbc\xb0\n\xe6\x9c\x89\xe9\x99\x90\xe5\x85\xac\xe5\x8f\xb8'.decode(), b'\xe7\xa8\xb3\xe5\xae\x9a'.decode(), ], ], [ [b'\xe5\xba\x8f\xe5\x8f\xb7'.decode(), b'\xe9\x87\x91\xe8\x9e\x8d\xe6\x9c\xba\xe6\x9e\x84\xe5\x90\x8d\xe7\xa7\xb0'.decode(), b'\xe6\x8e\x88\xe4\xbf\xa1\xe6\x80\xbb\xe9\xa2\x9d'.decode(), b'\xe5\xb7\xb2\xe4\xbd\xbf\xe7\x94\xa8\xe6\x83\x85\xe5\x86\xb5'.decode(), b'\xe5\x89\xa9\xe4\xbd\x99\xe9\xa2\x9d\xe5\xba\xa6'.decode(), ], - ['1', b'\xe5\xb7\xa5\xe5\x95\x86\xe9\x93\xb6\xe8\xa1\x8c'.decode(), '33043530\n, .', '23485000\n, .', '9558530\n, .', ], - ['2', b'\xe5\xbe\xbd\xe5\x95\x86\xe9\x93\xb6\xe8\xa1\x8c\xe5\x8d\x97\xe4\xba\xac\xe4\xb8\xad\xe5\xb1\xb1\xe5\x8c\x97\xe8\xb7\xaf\xe6\x94\xaf\xe8\xa1\x8c'.decode(), '33043530\n, .', '25673530\n, .', '7370000\n, .', ], - ['3', b'\xe5\x85\xb4\xe4\xb8\x9a\xe9\x93\xb6\xe8\xa1\x8c\xe7\x9b\x90\xe5\x9f\x8e\xe5\x88\x86\xe8\xa1\x8c'.decode(), '30260000\n, .', '11060000\n, .', '19200000\n, .', ], - ['4', b'\xe6\xb0\x91\xe7\x94\x9f\xe9\x93\xb6\xe8\xa1\x8c\xe7\x9b\x90\xe5\x9f\x8e\xe5\x88\x86\xe8\xa1\x8c'.decode(), '26200000\n, .', '8850000\n, .', '17350000\n, .', ], - ['5', b'\xe4\xb8\xad\xe4\xbf\xa1\xe9\x93\xb6\xe8\xa1\x8c\xe7\x9b\x90\xe5\x9f\x8e\xe5\x88\x86\xe8\xa1\x8c'.decode(), '20508800\n, .', '20508800\n, .', '000\n.', ], - ['6', b'\xe5\x8d\x97\xe4\xba\xac\xe9\x93\xb6\xe8\xa1\x8c\xe7\x9b\x90\xe5\x9f\x8e\xe5\x88\x86\xe8\xa1\x8c'.decode(), '17517000\n, .', '17517000\n, .', '000\n.', ], - ['7', b'\xe6\x9d\xad\xe5\xb7\x9e\xe9\x93\xb6\xe8\xa1\x8c'.decode(), '10900000\n, .', '3269000\n, .', '7631000\n, .', ], - ['8', b'\xe6\xb1\x9f\xe8\x8b\x8f\xe9\x93\xb6\xe8\xa1\x8c'.decode(), '21859700\n, .', '21859700\n, .', '000\n.', ], - ['9', b'\xe6\xb8\xa4\xe6\xb5\xb7\xe9\x93\xb6\xe8\xa1\x8c'.decode(), '9800000\n, .', '000\n.', '9800000\n, .', ], - ['10', b'\xe6\x81\x92\xe4\xb8\xb0\xe9\x93\xb6\xe8\xa1\x8c\xe5\x8d\x97\xe4\xba\xac\xe6\xb1\x9f\xe5\x8c\x97\xe6\x94\xaf\xe8\xa1\x8c'.decode(), '9390000\n, .', '4350000\n, .', '5040000\n, .', ], - ['11', b'\xe4\xb8\x8a\xe6\xb5\xb7\xe9\x93\xb6\xe8\xa1\x8c\xe7\x9b\x90\xe5\x9f\x8e\xe5\x88\x86\xe8\xa1\x8c'.decode(), '4626500\n, .', '4626500\n, .', '000\n.', ], + ['1', b'\xe5\xb7\xa5\xe5\x95\x86\xe9\x93\xb6\xe8\xa1\x8c'.decode(), '330,435.30', '234,850.00', '95,585.30', ], + ['2', b'\xe5\xbe\xbd\xe5\x95\x86\xe9\x93\xb6\xe8\xa1\x8c\xe5\x8d\x97\xe4\xba\xac\xe4\xb8\xad\xe5\xb1\xb1\xe5\x8c\x97\xe8\xb7\xaf\xe6\x94\xaf\xe8\xa1\x8c'.decode(), '330,435.30', '256,735.30', '73,700.00', ], + ['3', b'\xe5\x85\xb4\xe4\xb8\x9a\xe9\x93\xb6\xe8\xa1\x8c\xe7\x9b\x90\xe5\x9f\x8e\xe5\x88\x86\xe8\xa1\x8c'.decode(), '302,600.00', '110,600.00', '192,000.00', ], + ['4', b'\xe6\xb0\x91\xe7\x94\x9f\xe9\x93\xb6\xe8\xa1\x8c\xe7\x9b\x90\xe5\x9f\x8e\xe5\x88\x86\xe8\xa1\x8c'.decode(), '262,000.00', '88,500.00', '173,500.00', ], + ['5', b'\xe4\xb8\xad\xe4\xbf\xa1\xe9\x93\xb6\xe8\xa1\x8c\xe7\x9b\x90\xe5\x9f\x8e\xe5\x88\x86\xe8\xa1\x8c'.decode(), '205,088.00', '205,088.00', '0.00', ], + ['6', b'\xe5\x8d\x97\xe4\xba\xac\xe9\x93\xb6\xe8\xa1\x8c\xe7\x9b\x90\xe5\x9f\x8e\xe5\x88\x86\xe8\xa1\x8c'.decode(), '175,170.00', '175,170.00', '0.00', ], + ['7', b'\xe6\x9d\xad\xe5\xb7\x9e\xe9\x93\xb6\xe8\xa1\x8c'.decode(), '109,000.00', '32,690.00', '76,310.00', ], + ['8', b'\xe6\xb1\x9f\xe8\x8b\x8f\xe9\x93\xb6\xe8\xa1\x8c'.decode(), '218,597.00', '218,597.00', '0.00', ], + ['9', b'\xe6\xb8\xa4\xe6\xb5\xb7\xe9\x93\xb6\xe8\xa1\x8c'.decode(), '98,000.00', '0.00', '98,000.00', ], + ['10', b'\xe6\x81\x92\xe4\xb8\xb0\xe9\x93\xb6\xe8\xa1\x8c\xe5\x8d\x97\xe4\xba\xac\xe6\xb1\x9f\xe5\x8c\x97\xe6\x94\xaf\xe8\xa1\x8c'.decode(), '93,900.00', '43,500.00', '50,400.00', ], + ['11', b'\xe4\xb8\x8a\xe6\xb5\xb7\xe9\x93\xb6\xe8\xa1\x8c\xe7\x9b\x90\xe5\x9f\x8e\xe5\x88\x86\xe8\xa1\x8c'.decode(), '46,265.00', '46,265.00', '0.00', ], ], ] _check_tables(expected, [table.extract() for table in tables]) @@ -259,24 +259,24 @@ def test_table2(): expected = [ [ [b'\xe5\xb9\xb4\xe4\xbb\xbd'.decode(), b'\xe4\xb8\xbb\xe4\xbd\x93\xe4\xbf\xa1\xe7\x94\xa8\n\xe8\xaf\x84\xe7\xba\xa7'.decode(), b'\xe6\xb6\xb5\xe4\xb9\x89'.decode(), b'\xe8\xaf\x84\xe7\xba\xa7\xe6\x9c\xba\xe6\x9e\x84'.decode(), b'\xe8\xaf\x84\xe7\xba\xa7\xe5\xb1\x95\xe6\x9c\x9b'.decode(), ], - ['2023', 'AA+', b'\xe5\x81\xbf\xe8\xbf\x98\xe5\x80\xba\xe5\x8a\xa1\xe7\x9a\x84\xe8\x83\xbd\xe5\x8a\x9b\xe5\xbe\x88\xe5\xbc\xba \xe5\x8f\x97\xe4\xb8\x8d\xe5\x88\xa9\xe7\xbb\x8f\xe6\xb5\x8e\n\xef\xbc\x8c\n\xe7\x8e\xaf\xe5\xa2\x83\xe7\x9a\x84\xe5\xbd\xb1\xe5\x93\x8d\xe4\xb8\x8d\xe5\xa4\xa7 \xe8\xbf\x9d\xe7\xba\xa6\xe9\xa3\x8e\xe9\x99\xa9\xe5\xbe\x88\xe4\xbd\x8e\n\xef\xbc\x8c'.decode(), b'\xe4\xb8\xad\xe8\xaf\x9a\xe4\xbf\xa1\xe5\x9b\xbd\xe9\x99\x85\xe4\xbf\xa1\n\xe7\x94\xa8\xe8\xaf\x84\xe7\xba\xa7\xe6\x9c\x89\xe9\x99\x90\xe8\xb4\xa3\n\xe4\xbb\xbb\xe5\x85\xac\xe5\x8f\xb8 \xe8\x81\x94\xe5\x90\x88\xe8\xb5\x84\n\xe3\x80\x81\n\xe4\xbf\xa1\xe8\xaf\x84\xe4\xbc\xb0\xe6\x9c\x89\xe9\x99\x90\xe5\x85\xac\n\xe5\x8f\xb8'.decode(), b'\xe7\xa8\xb3\xe5\xae\x9a'.decode(), ], - ['2022', 'AA+', b'\xe5\x81\xbf\xe8\xbf\x98\xe5\x80\xba\xe5\x8a\xa1\xe7\x9a\x84\xe8\x83\xbd\xe5\x8a\x9b\xe5\xbe\x88\xe5\xbc\xba \xe5\x8f\x97\xe4\xb8\x8d\xe5\x88\xa9\xe7\xbb\x8f\xe6\xb5\x8e\n\xef\xbc\x8c\n\xe7\x8e\xaf\xe5\xa2\x83\xe7\x9a\x84\xe5\xbd\xb1\xe5\x93\x8d\xe4\xb8\x8d\xe5\xa4\xa7 \xe8\xbf\x9d\xe7\xba\xa6\xe9\xa3\x8e\xe9\x99\xa9\xe5\xbe\x88\xe4\xbd\x8e\n\xef\xbc\x8c'.decode(), b'\xe4\xb8\xad\xe8\xaf\x9a\xe4\xbf\xa1\xe5\x9b\xbd\xe9\x99\x85\xe4\xbf\xa1\n\xe7\x94\xa8\xe8\xaf\x84\xe7\xba\xa7\xe6\x9c\x89\xe9\x99\x90\xe8\xb4\xa3\n\xe4\xbb\xbb\xe5\x85\xac\xe5\x8f\xb8 \xe8\x81\x94\xe5\x90\x88\xe8\xb5\x84\n\xe3\x80\x81\n\xe4\xbf\xa1\xe8\xaf\x84\xe4\xbc\xb0\xe6\x9c\x89\xe9\x99\x90\xe5\x85\xac\n\xe5\x8f\xb8'.decode(), b'\xe7\xa8\xb3\xe5\xae\x9a'.decode(), ], - ['2021', 'AA+', b'\xe5\x81\xbf\xe8\xbf\x98\xe5\x80\xba\xe5\x8a\xa1\xe7\x9a\x84\xe8\x83\xbd\xe5\x8a\x9b\xe5\xbe\x88\xe5\xbc\xba \xe5\x8f\x97\xe4\xb8\x8d\xe5\x88\xa9\xe7\xbb\x8f\xe6\xb5\x8e\n\xef\xbc\x8c\n\xe7\x8e\xaf\xe5\xa2\x83\xe7\x9a\x84\xe5\xbd\xb1\xe5\x93\x8d\xe4\xb8\x8d\xe5\xa4\xa7 \xe8\xbf\x9d\xe7\xba\xa6\xe9\xa3\x8e\xe9\x99\xa9\xe5\xbe\x88\xe4\xbd\x8e\n\xef\xbc\x8c'.decode(), b'\xe8\x81\x94\xe5\x90\x88\xe8\xb5\x84\xe4\xbf\xa1\xe8\xaf\x84\xe4\xbc\xb0\n\xe6\x9c\x89\xe9\x99\x90\xe5\x85\xac\xe5\x8f\xb8'.decode(), b'\xe7\xa8\xb3\xe5\xae\x9a'.decode(), ], - ['2020', 'AA+', b'\xe5\x81\xbf\xe8\xbf\x98\xe5\x80\xba\xe5\x8a\xa1\xe7\x9a\x84\xe8\x83\xbd\xe5\x8a\x9b\xe5\xbe\x88\xe5\xbc\xba \xe5\x8f\x97\xe4\xb8\x8d\xe5\x88\xa9\xe7\xbb\x8f\xe6\xb5\x8e\n\xef\xbc\x8c\n\xe7\x8e\xaf\xe5\xa2\x83\xe7\x9a\x84\xe5\xbd\xb1\xe5\x93\x8d\xe4\xb8\x8d\xe5\xa4\xa7 \xe8\xbf\x9d\xe7\xba\xa6\xe9\xa3\x8e\xe9\x99\xa9\xe5\xbe\x88\xe4\xbd\x8e\n\xef\xbc\x8c'.decode(), b'\xe8\x81\x94\xe5\x90\x88\xe8\xb5\x84\xe4\xbf\xa1\xe8\xaf\x84\xe4\xbc\xb0\n\xe6\x9c\x89\xe9\x99\x90\xe5\x85\xac\xe5\x8f\xb8'.decode(), b'\xe7\xa8\xb3\xe5\xae\x9a'.decode(), ], + ['2023', 'AA+', b'\xe5\x81\xbf\xe8\xbf\x98\xe5\x80\xba\xe5\x8a\xa1\xe7\x9a\x84\xe8\x83\xbd\xe5\x8a\x9b\xe5\xbe\x88\xe5\xbc\xba\xef\xbc\x8c\xe5\x8f\x97\xe4\xb8\x8d\xe5\x88\xa9\xe7\xbb\x8f\xe6\xb5\x8e\n\xe7\x8e\xaf\xe5\xa2\x83\xe7\x9a\x84\xe5\xbd\xb1\xe5\x93\x8d\xe4\xb8\x8d\xe5\xa4\xa7\xef\xbc\x8c\xe8\xbf\x9d\xe7\xba\xa6\xe9\xa3\x8e\xe9\x99\xa9\xe5\xbe\x88\xe4\xbd\x8e'.decode(), b'\xe4\xb8\xad\xe8\xaf\x9a\xe4\xbf\xa1\xe5\x9b\xbd\xe9\x99\x85\xe4\xbf\xa1\n\xe7\x94\xa8\xe8\xaf\x84\xe7\xba\xa7\xe6\x9c\x89\xe9\x99\x90\xe8\xb4\xa3\n\xe4\xbb\xbb\xe5\x85\xac\xe5\x8f\xb8\xe3\x80\x81\xe8\x81\x94\xe5\x90\x88\xe8\xb5\x84\n\xe4\xbf\xa1\xe8\xaf\x84\xe4\xbc\xb0\xe6\x9c\x89\xe9\x99\x90\xe5\x85\xac\n\xe5\x8f\xb8'.decode(), b'\xe7\xa8\xb3\xe5\xae\x9a'.decode(), ], + ['2022', 'AA+', b'\xe5\x81\xbf\xe8\xbf\x98\xe5\x80\xba\xe5\x8a\xa1\xe7\x9a\x84\xe8\x83\xbd\xe5\x8a\x9b\xe5\xbe\x88\xe5\xbc\xba\xef\xbc\x8c\xe5\x8f\x97\xe4\xb8\x8d\xe5\x88\xa9\xe7\xbb\x8f\xe6\xb5\x8e\n\xe7\x8e\xaf\xe5\xa2\x83\xe7\x9a\x84\xe5\xbd\xb1\xe5\x93\x8d\xe4\xb8\x8d\xe5\xa4\xa7\xef\xbc\x8c\xe8\xbf\x9d\xe7\xba\xa6\xe9\xa3\x8e\xe9\x99\xa9\xe5\xbe\x88\xe4\xbd\x8e'.decode(), b'\xe4\xb8\xad\xe8\xaf\x9a\xe4\xbf\xa1\xe5\x9b\xbd\xe9\x99\x85\xe4\xbf\xa1\n\xe7\x94\xa8\xe8\xaf\x84\xe7\xba\xa7\xe6\x9c\x89\xe9\x99\x90\xe8\xb4\xa3\n\xe4\xbb\xbb\xe5\x85\xac\xe5\x8f\xb8\xe3\x80\x81\xe8\x81\x94\xe5\x90\x88\xe8\xb5\x84\n\xe4\xbf\xa1\xe8\xaf\x84\xe4\xbc\xb0\xe6\x9c\x89\xe9\x99\x90\xe5\x85\xac\n\xe5\x8f\xb8'.decode(), b'\xe7\xa8\xb3\xe5\xae\x9a'.decode(), ], + ['2021', 'AA+', b'\xe5\x81\xbf\xe8\xbf\x98\xe5\x80\xba\xe5\x8a\xa1\xe7\x9a\x84\xe8\x83\xbd\xe5\x8a\x9b\xe5\xbe\x88\xe5\xbc\xba\xef\xbc\x8c\xe5\x8f\x97\xe4\xb8\x8d\xe5\x88\xa9\xe7\xbb\x8f\xe6\xb5\x8e\n\xe7\x8e\xaf\xe5\xa2\x83\xe7\x9a\x84\xe5\xbd\xb1\xe5\x93\x8d\xe4\xb8\x8d\xe5\xa4\xa7\xef\xbc\x8c\xe8\xbf\x9d\xe7\xba\xa6\xe9\xa3\x8e\xe9\x99\xa9\xe5\xbe\x88\xe4\xbd\x8e'.decode(), b'\xe8\x81\x94\xe5\x90\x88\xe8\xb5\x84\xe4\xbf\xa1\xe8\xaf\x84\xe4\xbc\xb0\n\xe6\x9c\x89\xe9\x99\x90\xe5\x85\xac\xe5\x8f\xb8'.decode(), b'\xe7\xa8\xb3\xe5\xae\x9a'.decode(), ], + ['2020', 'AA+', b'\xe5\x81\xbf\xe8\xbf\x98\xe5\x80\xba\xe5\x8a\xa1\xe7\x9a\x84\xe8\x83\xbd\xe5\x8a\x9b\xe5\xbe\x88\xe5\xbc\xba\xef\xbc\x8c\xe5\x8f\x97\xe4\xb8\x8d\xe5\x88\xa9\xe7\xbb\x8f\xe6\xb5\x8e\n\xe7\x8e\xaf\xe5\xa2\x83\xe7\x9a\x84\xe5\xbd\xb1\xe5\x93\x8d\xe4\xb8\x8d\xe5\xa4\xa7\xef\xbc\x8c\xe8\xbf\x9d\xe7\xba\xa6\xe9\xa3\x8e\xe9\x99\xa9\xe5\xbe\x88\xe4\xbd\x8e'.decode(), b'\xe8\x81\x94\xe5\x90\x88\xe8\xb5\x84\xe4\xbf\xa1\xe8\xaf\x84\xe4\xbc\xb0\n\xe6\x9c\x89\xe9\x99\x90\xe5\x85\xac\xe5\x8f\xb8'.decode(), b'\xe7\xa8\xb3\xe5\xae\x9a'.decode(), ], ], [ [b'\xe5\xba\x8f\xe5\x8f\xb7'.decode(), b'\xe9\x87\x91\xe8\x9e\x8d\xe6\x9c\xba\xe6\x9e\x84\xe5\x90\x8d\xe7\xa7\xb0'.decode(), b'\xe6\x8e\x88\xe4\xbf\xa1\xe6\x80\xbb\xe9\xa2\x9d'.decode(), b'\xe5\xb7\xb2\xe4\xbd\xbf\xe7\x94\xa8\xe6\x83\x85\xe5\x86\xb5'.decode(), b'\xe5\x89\xa9\xe4\xbd\x99\xe9\xa2\x9d\xe5\xba\xa6'.decode(), ], - ['1', b'\xe5\xb7\xa5\xe5\x95\x86\xe9\x93\xb6\xe8\xa1\x8c'.decode(), '33043530\n, .', '23485000\n, .', '9558530\n, .', ], - ['2', b'\xe5\xbe\xbd\xe5\x95\x86\xe9\x93\xb6\xe8\xa1\x8c\xe5\x8d\x97\xe4\xba\xac\xe4\xb8\xad\xe5\xb1\xb1\xe5\x8c\x97\xe8\xb7\xaf\xe6\x94\xaf\xe8\xa1\x8c'.decode(), '33043530\n, .', '25673530\n, .', '7370000\n, .', ], - ['3', b'\xe5\x85\xb4\xe4\xb8\x9a\xe9\x93\xb6\xe8\xa1\x8c\xe7\x9b\x90\xe5\x9f\x8e\xe5\x88\x86\xe8\xa1\x8c'.decode(), '30260000\n, .', '11060000\n, .', '19200000\n, .', ], - ['4', b'\xe6\xb0\x91\xe7\x94\x9f\xe9\x93\xb6\xe8\xa1\x8c\xe7\x9b\x90\xe5\x9f\x8e\xe5\x88\x86\xe8\xa1\x8c'.decode(), '26200000\n, .', '8850000\n, .', '17350000\n, .', ], - ['5', b'\xe4\xb8\xad\xe4\xbf\xa1\xe9\x93\xb6\xe8\xa1\x8c\xe7\x9b\x90\xe5\x9f\x8e\xe5\x88\x86\xe8\xa1\x8c'.decode(), '20508800\n, .', '20508800\n, .', '000\n.', ], - ['6', b'\xe5\x8d\x97\xe4\xba\xac\xe9\x93\xb6\xe8\xa1\x8c\xe7\x9b\x90\xe5\x9f\x8e\xe5\x88\x86\xe8\xa1\x8c'.decode(), '17517000\n, .', '17517000\n, .', '000\n.', ], - ['7', b'\xe6\x9d\xad\xe5\xb7\x9e\xe9\x93\xb6\xe8\xa1\x8c'.decode(), '10900000\n, .', '3269000\n, .', '7631000\n, .', ], - ['8', b'\xe6\xb1\x9f\xe8\x8b\x8f\xe9\x93\xb6\xe8\xa1\x8c'.decode(), '21859700\n, .', '21859700\n, .', '000\n.', ], - ['9', b'\xe6\xb8\xa4\xe6\xb5\xb7\xe9\x93\xb6\xe8\xa1\x8c'.decode(), '9800000\n, .', '000\n.', '9800000\n, .', ], - ['10', b'\xe6\x81\x92\xe4\xb8\xb0\xe9\x93\xb6\xe8\xa1\x8c\xe5\x8d\x97\xe4\xba\xac\xe6\xb1\x9f\xe5\x8c\x97\xe6\x94\xaf\xe8\xa1\x8c'.decode(), '9390000\n, .', '4350000\n, .', '5040000\n, .', ], - ['11', b'\xe4\xb8\x8a\xe6\xb5\xb7\xe9\x93\xb6\xe8\xa1\x8c\xe7\x9b\x90\xe5\x9f\x8e\xe5\x88\x86\xe8\xa1\x8c'.decode(), '4626500\n, .', '4626500\n, .', '000\n.', ], + ['1', b'\xe5\xb7\xa5\xe5\x95\x86\xe9\x93\xb6\xe8\xa1\x8c'.decode(), '330,435.30', '234,850.00', '95,585.30', ], + ['2', b'\xe5\xbe\xbd\xe5\x95\x86\xe9\x93\xb6\xe8\xa1\x8c\xe5\x8d\x97\xe4\xba\xac\xe4\xb8\xad\xe5\xb1\xb1\xe5\x8c\x97\xe8\xb7\xaf\xe6\x94\xaf\xe8\xa1\x8c'.decode(), '330,435.30', '256,735.30', '73,700.00', ], + ['3', b'\xe5\x85\xb4\xe4\xb8\x9a\xe9\x93\xb6\xe8\xa1\x8c\xe7\x9b\x90\xe5\x9f\x8e\xe5\x88\x86\xe8\xa1\x8c'.decode(), '302,600.00', '110,600.00', '192,000.00', ], + ['4', b'\xe6\xb0\x91\xe7\x94\x9f\xe9\x93\xb6\xe8\xa1\x8c\xe7\x9b\x90\xe5\x9f\x8e\xe5\x88\x86\xe8\xa1\x8c'.decode(), '262,000.00', '88,500.00', '173,500.00', ], + ['5', b'\xe4\xb8\xad\xe4\xbf\xa1\xe9\x93\xb6\xe8\xa1\x8c\xe7\x9b\x90\xe5\x9f\x8e\xe5\x88\x86\xe8\xa1\x8c'.decode(), '205,088.00', '205,088.00', '0.00', ], + ['6', b'\xe5\x8d\x97\xe4\xba\xac\xe9\x93\xb6\xe8\xa1\x8c\xe7\x9b\x90\xe5\x9f\x8e\xe5\x88\x86\xe8\xa1\x8c'.decode(), '175,170.00', '175,170.00', '0.00', ], + ['7', b'\xe6\x9d\xad\xe5\xb7\x9e\xe9\x93\xb6\xe8\xa1\x8c'.decode(), '109,000.00', '32,690.00', '76,310.00', ], + ['8', b'\xe6\xb1\x9f\xe8\x8b\x8f\xe9\x93\xb6\xe8\xa1\x8c'.decode(), '218,597.00', '218,597.00', '0.00', ], + ['9', b'\xe6\xb8\xa4\xe6\xb5\xb7\xe9\x93\xb6\xe8\xa1\x8c'.decode(), '98,000.00', '0.00', '98,000.00', ], + ['10', b'\xe6\x81\x92\xe4\xb8\xb0\xe9\x93\xb6\xe8\xa1\x8c\xe5\x8d\x97\xe4\xba\xac\xe6\xb1\x9f\xe5\x8c\x97\xe6\x94\xaf\xe8\xa1\x8c'.decode(), '93,900.00', '43,500.00', '50,400.00', ], + ['11', b'\xe4\xb8\x8a\xe6\xb5\xb7\xe9\x93\xb6\xe8\xa1\x8c\xe7\x9b\x90\xe5\x9f\x8e\xe5\x88\x86\xe8\xa1\x8c'.decode(), '46,265.00', '46,265.00', '0.00', ], ], ] _check_tables(expected, tables2) @@ -601,10 +601,10 @@ def test_add_lines(): expected = [ [ [b'Boiling Points \xc2\xb0C'.decode(), 'min', 'max', 'avg'], - ['Noble gases', '269\n-', '62\n-', '170 5\n- .'], - ['Nonmetals', '253\n-', '4827', '414 1\n.'], - ['Metalloids', '335', '3900', '741 5\n.'], - ['Metals', '357', '>5000', '2755 9\n.'], + ['Noble gases', '-269', '-62', '-170.5'], + ['Nonmetals', '-253', '4827', '414.1'], + ['Metalloids', '335', '3900', '741.5'], + ['Metals', '357', '>5000', '2755.9'], ], ] _check_tables(expected, [table.extract() for table in tabs1]) @@ -822,10 +822,10 @@ def test_boxes_param(): expected = [ [ [b'Boiling Points \xc2\xb0C'.decode(), 'min', 'max', 'avg'], - ['Noble gases', '269\n-', '62\n-', '170 5\n- .'], - ['Nonmetals', '253\n-', '4827', '414 1\n.'], - ['Metalloids', '335', '3900', '741 5\n.'], - ['Metals', '357', '>5000', '2755 9\n.'], + ['Noble gases', '-269', '-62', '-170.5'], + ['Nonmetals', '-253', '4827', '414.1'], + ['Metalloids', '335', '3900', '741.5'], + ['Metals', '357', '>5000', '2755.9'], ], ] _check_tables(expected, [tabs.tables[0].extract()]) @@ -849,45 +849,45 @@ def test_dotted_grid(): if util._use_layout(): expected = [ [ - ['REGIONE', "PROVINCIA/\nCITTA'\nMETROPOLITANA", 'COMUNE', 'NUMERO\nCOMUNI', 'COMUNI\nCAPOLUOGO\n(CAP)', 'COMUNI\nSUPERIORI\n15000\n.\nabitanti\n(SUP)', 'COMUNI\nPARIO\nINFERIORI\n15000\n.\nabitanti\n(INF)', 'COMUNIAL\nRINNOVOPER\nMOTIVIDIVERSI\nDASCADENZA\nNATURALE(*)', 'COMUNI\nSCIOLTIAL\nRINNOVOPER\nSCADENZA\nNATURALE(#)', 'POPOLAZIONE\nal31/12/2021\nDPR20/01/2023\n...', 'SEZIONI\nRilevazioneal\n31/12/2023', 'ELETTORI\nRilevazioneal\n31/12/2023'], + ['REGIONE', "PROVINCIA/\nCITTA'\nMETROPOLITANA", 'COMUNE', 'NUMERO\nCOMUNI', 'COMUNI\nCAPOLUOGO\n(CAP)', 'COMUNI\nSUPERIORI\n15.000\nabitanti\n(SUP)', 'COMUNI\nPARI O\nINFERIORI\n15.000\nabitanti\n(INF)', 'COMUNI AL\nRINNOVO PER\nMOTIVI DIVERSI\nDA SCADENZA\nNATURALE (*)', 'COMUNI\nSCIOLTI AL\nRINNOVO PER\nSCADENZA\nNATURALE (#)', 'POPOLAZIONE\nal 31/12/2021\nD.P.R. 20/01/2023', 'SEZIONI\nRilevazione al\n31/12/2023', 'ELETTORI\nRilevazione al\n31/12/2023'], ['', '', 'CORVARA', '1', '', '', 'INF', '', '', '206', '1', '510'], - ['', '', 'FARINDOLA', '1', '', '', 'INF', '', '', '1357\n.', '2', '1578\n.'], - ['', '', 'LETTOMANOPPELLO', '1', '', '', 'INF', '', '', '2713\n.', '3', '2710\n.'], - ['', '', 'MONTEBELLODIBERTONA', '1', '', '', 'INF', '', '', '883', '2', '1139\n.'], - ['', '', 'MONTESILVANO', '1', '', 'SUP', '', '', '', '53402\n.', '52', '44600\n.'], - ['', '', 'MOSCUFO', '1', '', '', 'INF', '', '', '3092\n.', '3', '2851\n.'], - ['', '', 'ROSCIANO', '1', '', '', 'INF', '', '', '4038\n.', '6', '3697\n.'], + ['', '', 'FARINDOLA', '1', '', '', 'INF', '', '', '1.357', '2', '1.578'], + ['', '', 'LETTOMANOPPELLO', '1', '', '', 'INF', '', '', '2.713', '3', '2.710'], + ['', '', 'MONTEBELLO DI BERTONA', '1', '', '', 'INF', '', '', '883', '2', '1.139'], + ['', '', 'MONTESILVANO', '1', '', 'SUP', '', '', '', '53.402', '52', '44.600'], + ['', '', 'MOSCUFO', '1', '', '', 'INF', '', '', '3.092', '3', '2.851'], + ['', '', 'ROSCIANO', '1', '', '', 'INF', '', '', '4.038', '6', '3.697'], ['', '', 'SALLE', '1', '', '', 'INF', '', '', '270', '1', '698'], ['', '', 'VICOLI', '1', '', '', 'INF', '', '', '381', '1', '495'], - ['', 'Totale', '', '17', '1', '2', '15', '0', '0', '209303\n.', '267', '185620\n.'], + ['', 'Totale', '', '17', '1', '2', '15', '0', '0', '209.303', '267', '185.620'], ['', '', '', '', '', '', '', '', '', '', '', ''], ['', 'TERAMO', '', '', '', '', '', '', '', '', '', ''], - ['', '', 'ANCARANO', '1', '', '', 'INF', '', '', '1811\n.', '2', '1620\n.'], + ['', '', 'ANCARANO', '1', '', '', 'INF', '', '', '1.811', '2', '1.620'], ['', '', 'ARSITA', '1', '', '', 'INF', '', '', '756', '1', '874'], - ['', '', 'ATRI', '1', '', '', 'INF', '*', '', '10064\n.', '14', '9986\n.'], - ['', '', 'CAMPLI', '1', '', '', 'INF', '', '', '6630\n.', '11', '6568\n.'], - ['', '', 'CANZANO', '1', '', '', 'INF', '', '', '1794\n.', '3', '1653\n.'], - ['', '', 'CASTIGLIONEMESSERRAIMONDO', '1', '', '', 'INF', '', '', '2052\n.', '4', '1917\n.'], - ['', '', 'CELLINOATTANASIO', '1', '', '', 'INF', '', '', '2274\n.', '3', '2531\n.'], - ['', '', 'CERMIGNANO', '1', '', '', 'INF', '', '', '1459\n.', '3', '1839\n.'], - ['', '', 'COLLEDARA', '1', '', '', 'INF', '', '', '2097\n.', '4', '2281\n.'], - ['', '', 'CORROPOLI', '1', '', '', 'INF', '', '', '5108\n.', '5', '4467\n.'], - ['', '', 'FANOADRIANO', '1', '', '', 'INF', '', '', '257', '2', '277'], - ['', '', 'GIULIANOVA', '1', '', 'SUP', '', '', '', '23442\n.', '23', '21806\n.'], + ['', '', 'ATRI', '1', '', '', 'INF', '*', '', '10.064', '14', '9.986'], + ['', '', 'CAMPLI', '1', '', '', 'INF', '', '', '6.630', '11', '6.568'], + ['', '', 'CANZANO', '1', '', '', 'INF', '', '', '1.794', '3', '1.653'], + ['', '', 'CASTIGLIONE MESSER RAIMONDO', '1', '', '', 'INF', '', '', '2.052', '4', '1.917'], + ['', '', 'CELLINO ATTANASIO', '1', '', '', 'INF', '', '', '2.274', '3', '2.531'], + ['', '', 'CERMIGNANO', '1', '', '', 'INF', '', '', '1.459', '3', '1.839'], + ['', '', 'COLLEDARA', '1', '', '', 'INF', '', '', '2.097', '4', '2.281'], + ['', '', 'CORROPOLI', '1', '', '', 'INF', '', '', '5.108', '5', '4.467'], + ['', '', 'FANO ADRIANO', '1', '', '', 'INF', '', '', '257', '2', '277'], + ['', '', 'GIULIANOVA', '1', '', 'SUP', '', '', '', '23.442', '23', '21.806'], ['', '', 'MONTEFINO', '1', '', '', 'INF', '', '', '967', '2', '915'], - ['', '', "MORROD'ORO", '1', '', '', 'INF', '', '', '3560\n.', '3', '3232\n.'], - ['', '', "MOSCIANOSANT'ANGELO", '1', '', '', 'INF', '', '', '9088\n.', '9', '8365\n.'], - ['', '', "PENNASANT'ANDREA", '1', '', '', 'INF', '', '', '1635\n.', '2', '1778\n.'], - ['', '', 'PINETO', '1', '', '', 'INF', '', '#', '14538\n.', '11', '13363\n.'], - ['', '', 'ROCCASANTAMARIA', '1', '', '', 'INF', '', '', '477', '1', '537'], - ['', '', "SANT'EGIDIOALLAVIBRATA", '1', '', '', 'INF', '', '', '9804\n.', '7', '8244\n.'], - ['', '', "SANT'OMERO", '1', '', '', 'INF', '', '', '5112\n.', '7', '4767\n.'], - ['', '', 'TORANONUOVO', '1', '', '', 'INF', '', '', '1490\n.', '2', '1280\n.'], - ['', '', 'TORRICELLASICURA', '1', '', '', 'INF', '', '', '2460\n.', '4', '2658\n.'], - ['', '', 'TOSSICIA', '1', '', '', 'INF', '', '#', '1258\n.', '3', '1388\n.'], - ['', 'Totale', '', '23', '0', '1', '22', '1', '2', '108133\n.', '126', '102346\n.'], + ['', '', "MORRO D'ORO", '1', '', '', 'INF', '', '', '3.560', '3', '3.232'], + ['', '', "MOSCIANO SANT'ANGELO", '1', '', '', 'INF', '', '', '9.088', '9', '8.365'], + ['', '', "PENNA SANT'ANDREA", '1', '', '', 'INF', '', '', '1.635', '2', '1.778'], + ['', '', 'PINETO', '1', '', '', 'INF', '', '#', '14.538', '11', '13.363'], + ['', '', 'ROCCA SANTA MARIA', '1', '', '', 'INF', '', '', '477', '1', '537'], + ['', '', "SANT'EGIDIO ALLA VIBRATA", '1', '', '', 'INF', '', '', '9.804', '7', '8.244'], + ['', '', "SANT'OMERO", '1', '', '', 'INF', '', '', '5.112', '7', '4.767'], + ['', '', 'TORANO NUOVO', '1', '', '', 'INF', '', '', '1.490', '2', '1.280'], + ['', '', 'TORRICELLA SICURA', '1', '', '', 'INF', '', '', '2.460', '4', '2.658'], + ['', '', 'TOSSICIA', '1', '', '', 'INF', '', '#', '1.258', '3', '1.388'], + ['', 'Totale', '', '23', '0', '1', '22', '1', '2', '108.133', '126', '102.346'], ['', '', '', '', '', '', '', '', '', '', '', ''], - ['BRUZZO', '', '', '98', '1', '3', '95', '2', '2', '424264\n.', '537', '405785\n.'], + ['BRUZZO', '', '', '98', '1', '3', '95', '2', '2', '424.264', '537', '405.785'], ], ] _check_tables(expected, [table.extract() for table in tabs]) @@ -924,126 +924,55 @@ def test_4017(): if util._use_layout(): - if util._use_4llm(): - tables2_expected = [ - [ - ['ClassA/BOvercollateralization', '13144%\n.', '>=', '12260%\n.', '', 'PASS', ], - [None, None, None, None, None, 'PASS', ], - ['ClassDOvercollateralization', '11224%\n.', '>=', '10640%\n.', '', 'PASS', ], - [None, None, None, None, None, 'PASS', ], - ['EventofDefault', '15608%\n.', '>=', '10250%\n.', '', 'PASS', ], - [None, None, None, None, None, 'PASS', ], - ['ClassA/BInterestCoverage', 'N/A', '>=', '12000%\n.', '', 'N/A', ], - [None, None, None, None, None, 'N/A', ], - ['ClassDInterestCoverage', 'N/A', '>=', '10500%\n.', '', 'N/A', ], - ], - [ - ["Moody'sMaximumRatingFactorTest", '2577\n,', '<=', '3250\n,', '', 'PASS', '2581\n,', ], - [None, None, None, None, None, 'PASS', None, ], - ['MinimumFloatingSpread', '35006%\n.', '>=', '20000%\n.', '', 'PASS', '34871%\n.', ], - [None, None, None, None, None, 'PASS', None, ], - ['MinimumWeightedAverageS&PRecovery\nRateTest', '4050%\n.', '>=', '4000%\n.', '', 'PASS', '4040%\n.', ], - [None, None, None, None, None, 'PASS', None, ], - ['WeightedAverageLife', '483\n.', '<=', '900\n.', '', 'PASS', '492\n.', ], - ], - [ - ['Issuer', 'OCPCLO202432 LTD\n- , .', '', '', 'Count', '', '', 'Current', ], - ['Co-Issuer', 'OCPCLO202432LLC\n-', 'DelayedDrawLoan', '', '1', '', '', '6500000\n, .', ], - ['CollateralTrustee', 'Citibank N .A\n.', 'RevolvingLoan', '', '0', '', '', '000\n.', ], - ['CollateralManager', 'Onex Credit Partners, LLC', 'TermLoan', '', '293', '', '', '37486898542\n, , .', ], - ['RatingAgencies\nCollateralAdministrator', 'S&P\nSiepeLLC', 'MiscellaneousInformation', None, None, None, None, '', ], - ['RelationshipManager', 'SabrinaSchmidt', 'AggregatePrincipalBalance', None, '', '', '', '38603398542\n, , .', ], - ['', 'sschmidt@siepe.com', 'PrincipalCash', '', '', '', '', '1281594162\n, , .', ], - ['', '2818704754\n- -', 'PFAI', '', '', '', '', '72757624\n, .', ], - ['ClosingDate', '04/23/2024', 'Totals', '', '', '', '', '39957750328\n, , .', ], - ['FirstPaymentDate', '10/23/2024', '', '', '', '', '', '', ], - ['ReinvestmentPeriod\nEffectiveDate', '04/23/2024 04/23/2029\n-\n04/25/2024', 'AccountBalances', None, '', '', '', '', ], - ['NextPaymentDate\nPriorPaymentDate', '10/23/2024\n-', 'PrincipalCash\nInterestCash', '', '', '', '', '1281594162\n, , .\n1300009090\n, , .', ], - ['CollectionPeriod', '04/23/2024 10/08/2024\n-', '', '', '', '', '', '', ], - ], - [ - ['Notes', 'OriginalBalance', 'CurrentBalance', 'Spread', 'Coupon', 'Interest', ], - ['Class A -1 Notes', '256000000\n, ,', '256000000\n, ,', '152000%\n.', '682458%\n.', '888105344\n, , .', ], - ['Class A -2 Notes', '16000000\n, ,', '16000000\n, ,', '172000%\n.', '702458%\n.', '57133251\n, .', ], - ['Class B -1 Notes', '24000000\n, ,', '24000000\n, ,', '200000%\n.', '730458%\n.', '89115876\n, .', ], - ['Class B -2 Notes', '8000000\n, ,', '8000000\n, ,', '584100%\n.', '584100%\n.', '23364000\n, .', ], - ['ClassCNotes', '24000000\n, ,', '24000000\n, ,', '250000%\n.', '780458%\n.', '95215876\n, .', ], - ['Class D -1 Notes', '24000000\n, ,', '24000000\n, ,', '375000%\n.', '905458%\n.', '110465876\n, , .', ], - ['Class D -2 Notes', '4000000\n, ,', '4000000\n, ,', '905000%\n.', '905000%\n.', '18100000\n, .', ], - ['ClassENotes', '12000000\n, ,', '12000000\n, ,', '676000%\n.', '1206458%\n.', '73593938\n, .', ], - ['', '36800000000\n, , .', '36800000000\n, , .', '', '', '1355094161\n, , .', ], - ], - ] - else: - - tables2_expected = [ - [ - ['ClassA/BOvercollateralization', '13144%\n.', '>=', '12260%\n.', '', 'PASS'], - [None, None, None, None, None, 'PASS'], - ['ClassDOvercollateralization', '11224%\n.', '>=', '10640%\n.', '', 'PASS'], - [None, None, None, None, None, 'PASS'], - ['EventofDefault', '15608%\n.', '>=', '10250%\n.', '', 'PASS'], - [None, None, None, None, None, 'PASS'], - ['ClassA/BInterestCoverage', 'N/A', '>=', '12000%\n.', '', 'N/A'], - [None, None, None, None, None, 'N/A'], - ['ClassDInterestCoverage', 'N/A', '>=', '10500%\n.', '', 'N/A'], - ], - [ - ["Moody'sMaximumRatingFactorTest", '2577\n,', '<=', '3250\n,', '', 'PASS', '2581\n,'], - [None, None, None, None, None, 'PASS', None], - ['MinimumFloatingSpread', '35006%\n.', '>=', '20000%\n.', '', 'PASS', '34871%\n.'], - [None, None, None, None, None, 'PASS', None], - ['MinimumWeightedAverageS&PRecovery\nRateTest', '4050%\n.', '>=', '4000%\n.', '', 'PASS', '4040%\n.'], - [None, None, None, None, None, 'PASS', None], - ['WeightedAverageLife', '483\n.', '<=', '900\n.', '', 'PASS', '492\n.'], - ], - [ - ['AssetDetails', '', '', '', '', '', '', 'Notes'], - ['', '', 'Count', '', '', 'Current', '', 'Notes'], - ['DelayedDrawLoan', '', '1', '', '', '6500000\n, .', '', 'Class A -1'], - ['RevolvingLoan', '', '0', '', '', '000\n.', '', 'Class A -2'], - ['TermLoan', '', '293', '', '', '37486898542\n, , .', '', 'Class B -1'], - ['MiscellaneousInformation', None, None, None, None, '', '', 'Class B -2\nClassC'], - ['', '', '', '', '', '', '', ''], - ['AggregatePrincipalBalance', None, '', '', '', '38603398542\n, , .', '', 'Class D\n-'], - ['PrincipalCash', '', '', '', '', '1281594162\n, , .', '', 'Class D\n-'], - ['PFAI', '', '', '', '', '72757624\n, .', '', 'ClassEN'], - ['Totals', '', '', '', '', '39957750328\n, , .', '', ''], - ['AccountBalances', None, '', '', '', '', '', ''], - ['', '', '', '', '', '', '', ''], - ['PrincipalCash\nInterestCash', '', '', '', '', '1281594162\n, , .\n1300009090\n, , .', '', ''], - ], - [ - ['Issuer', 'OCPCLO202432 LTD\n- , .'], - ['Co-Issuer', 'OCPCLO202432LLC\n-'], - ['CollateralTrustee', 'Citibank N .A\n.'], - ['CollateralManager', 'Onex Credit Partners, LLC'], - ['RatingAgencies', 'S&P'], - ['CollateralAdministrator', 'SiepeLLC'], - ['RelationshipManager', 'SabrinaSchmidt'], - ['', 'sschmidt@siepe.com'], - ['', '2818704754\n- -'], - ['ClosingDate', '04/23/2024'], - ['FirstPaymentDate', '10/23/2024'], - ['ReinvestmentPeriod', '04/23/2024 04/23/2029\n-'], - ['EffectiveDate', '04/25/2024'], - ['NextPaymentDate', '10/23/2024'], - ['PriorPaymentDate', '-'], - ['CollectionPeriod', '04/23/2024 10/08/2024\n-'], - ], - [ - ['Notes', 'OriginalBalance', 'CurrentBalance', 'Spread', 'Coupon', 'Interest'], - ['Class A -1 Notes', '256000000\n, ,', '256000000\n, ,', '152000%\n.', '682458%\n.', '888105344\n, , .'], - ['Class A -2 Notes', '16000000\n, ,', '16000000\n, ,', '172000%\n.', '702458%\n.', '57133251\n, .'], - ['Class B -1 Notes', '24000000\n, ,', '24000000\n, ,', '200000%\n.', '730458%\n.', '89115876\n, .'], - ['Class B -2 Notes', '8000000\n, ,', '8000000\n, ,', '584100%\n.', '584100%\n.', '23364000\n, .'], - ['ClassCNotes', '24000000\n, ,', '24000000\n, ,', '250000%\n.', '780458%\n.', '95215876\n, .'], - ['Class D -1 Notes', '24000000\n, ,', '24000000\n, ,', '375000%\n.', '905458%\n.', '110465876\n, , .'], - ['Class D -2 Notes', '4000000\n, ,', '4000000\n, ,', '905000%\n.', '905000%\n.', '18100000\n, .'], - ['ClassENotes', '12000000\n, ,', '12000000\n, ,', '676000%\n.', '1206458%\n.', '73593938\n, .'], - ['', '36800000000\n, , .', '36800000000\n, , .', '', '', '1355094161\n, , .'], - ], - ] + tables2_expected = [ + [ + ['Class A/B Overcollateralization', '131.44%', '>=', '122.60%', '', 'PASS', ], + [None, None, None, None, None, 'PASS', ], + ['Class D Overcollateralization', '112.24%', '>=', '106.40%', '', 'PASS', ], + [None, None, None, None, None, 'PASS', ], + ['Event of Default', '156.08%', '>=', '102.50%', '', 'PASS', ], + [None, None, None, None, None, 'PASS', ], + ['Class A/B Interest Coverage', 'N/A', '>=', '120.00%', '', 'N/A', ], + [None, None, None, None, None, 'N/A', ], + ['Class D Interest Coverage', 'N/A', '>=', '105.00%', '', 'N/A', ], + ], + [ + ["Moody's Maximum Rating Factor Test", '2,577', '<=', '3,250', '', 'PASS', '2,581', ], + [None, None, None, None, None, 'PASS', None, ], + ['Minimum Floating Spread', '3.5006%', '>=', '2.0000%', '', 'PASS', '3.4871%', ], + [None, None, None, None, None, 'PASS', None, ], + ['Minimum Weighted Average S&P Recovery\nRate Test', '40.50%', '>=', '40.00%', '', 'PASS', '40.40%', ], + [None, None, None, None, None, 'PASS', None, ], + ['Weighted Average Life', '4.83', '<=', '9.00', '', 'PASS', '4.92', ], + ], + [ + ['Issuer', 'OCP CLO 2024-32, LTD.', '', '', 'Count', '', '', 'Current', ], + ['Co-Issuer', 'OCP CLO 2024-32 LLC', 'Delayed Draw Loan', '', '1', '', '', '65,000.00', ], + ['Collateral Trustee', 'Citibank N.A.', 'Revolving Loan', '', '0', '', '', '0.00', ], + ['Collateral Manager', 'Onex Credit Partners, LLC', 'Term Loan', '', '293', '', '', '374,868,985.42', ], + ['Rating Agencies\nCollateral Administrator', 'S&P\nSiepe LLC', 'Miscellaneous Information', None, None, None, None, '', ], + ['Relationship Manager', 'Sabrina Schmidt', 'Aggregate Principal Balance', None, '', '', '', '386,033,985.42', ], + ['', 'sschmidt@siepe.com', 'Principal Cash', '', '', '', '', '12,815,941.62', ], + ['', '281-870-4754', 'PFAI', '', '', '', '', '727,576.24', ], + ['Closing Date', '04/23/2024', 'Totals', '', '', '', '', '399,577,503.28', ], + ['First Payment Date', '10/23/2024', '', '', '', '', '', '', ], + ['Reinvestment Period\nEffective Date', '04/23/2024 - 04/23/2029\n04/25/2024', 'Account Balances', None, '', '', '', '', ], + ['Next Payment Date\nPrior Payment Date', '10/23/2024\n-', 'Principal Cash\nInterest Cash', '', '', '', '', '12,815,941.62\n13,000,090.90', ], + ['Collection Period', '04/23/2024 - 10/08/2024', '', '', '', '', '', '', ], + ], + [ + ['Notes', 'Original Balance', 'Current Balance', 'Spread', 'Coupon', 'Interest', ], + ['Class A-1 Notes', '256,000,000', '256,000,000', '1.52000%', '6.82458%', '8,881,053.44', ], + ['Class A-2 Notes', '16,000,000', '16,000,000', '1.72000%', '7.02458%', '571,332.51', ], + ['Class B-1 Notes', '24,000,000', '24,000,000', '2.00000%', '7.30458%', '891,158.76', ], + ['Class B-2 Notes', '8,000,000', '8,000,000', '5.84100%', '5.84100%', '233,640.00', ], + ['Class C Notes', '24,000,000', '24,000,000', '2.50000%', '7.80458%', '952,158.76', ], + ['Class D-1 Notes', '24,000,000', '24,000,000', '3.75000%', '9.05458%', '1,104,658.76', ], + ['Class D-2 Notes', '4,000,000', '4,000,000', '9.05000%', '9.05000%', '181,000.00', ], + ['Class E Notes', '12,000,000', '12,000,000', '6.76000%', '12.06458%', '735,939.38', ], + ['', '368,000,000.00', '368,000,000.00', '', '', '13,550,941.61', ], + ], + ] _check_tables(tables2_expected, tables2) @@ -1105,7 +1034,7 @@ def test_md_styles(): tabs = page.find_tables()[0] if util._use_layout(): text = textwrap.dedent(''' - |Column1|Column2|Column3| + |Column 1|Column 2|Column 3| |---|---|---| |Zelle (0,0)|**Bold (0,1)**|Zelle (0,2)| |~~Strikeout (1,0), Zeile 1~~
~~Hier kommt Zeile 2.~~|Zelle (1,1)|~~Strikeout (1,2)~~|