diff --git a/src/table.py b/src/table.py index af6dee798..0348eae45 100644 --- a/src/table.py +++ b/src/table.py @@ -703,6 +703,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, @@ -735,6 +759,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 @@ -768,6 +795,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( @@ -812,8 +845,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"] @@ -826,8 +859,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"] @@ -882,11 +915,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: @@ -959,13 +992,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)]) @@ -2418,6 +2451,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 ) @@ -2433,6 +2481,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), @@ -2454,6 +2503,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 0bc344af4..28c114176 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)~~|