Pascals Dreieck

Hintergrund

Piko hat die Aufgabe gestellt, ein (Python-)Programm zu schreiben, das das Pascalsche Dreieck nett formatiert ausgibt.

Meine Interpretation

Ich hab meine eigene Idee, was “nett formatiert” bedeutet:

Hier, wie so ein Dreieck aussieht:

                                    1
                                 1     1
                              1     2     1
                           1     3     3     1
                         1    4     6     4     1
                      1    5     10    10    5    1
                    1    6    15    20    15    6    1
                 1    7    21    35    35   21    7    1
               1    8   28    56    70    56   28    8    1
            1    9    36   84   126   126   84    36   9    1
          1   10   45   120  210   252   210   120  45   10   1
        1   11   55  165  330   462   462   330  165   55  11   1
      1   12  66   220  495  792   924   792   495  220  66   12  1
    1   13  78  286  715  1287  1716  1716 1287  715  286  78   13  1
   1  14  91  364 1001 2002  3003  3432  3003 2002 1001  364  91  14  1
 1  15 105 455  1365 3003 5005  6435  6435 5005  3003 1365 455 105 15  1
1 16 120 560 1820 4368 8008 11440 12870 11440 8008 4368 1820 560 120 16 1

Der Code

Und hier mein Code:

#!/usr/bin/env python3

# Code by DJ3EI dj3ei@famsik.de who places this work into the public domain.
# You can use it however you want, or, if you prefer, under the rules of the
# CC0 1.0 license https://creativecommons.org/publicdomain/zero/1.0/ .

# This was inspired by https://chaos.social/@piko/114490917474879618

from io import TextIOBase
from itertools import chain
from sys import stdout
from typing import cast


def generate_pascal_triangle(wanted_lines: int) -> list[list[int]]:
    if wanted_lines < 1:
        raise ValueError(
            "Don't know how to construct a Pascal triangle "
            f"with {wanted_lines} lines."
        )
    result = [[1]]
    one_zero = [0]
    for line_no in range(1, wanted_lines):
        previous_line = result[-1]
        result.append(
            list(
                map(
                    lambda x, y: x + y,
                    chain(one_zero, previous_line),
                    chain(previous_line, one_zero),
                )
            )
        )
    return result


def format_pascal_triangle(triangle: list[list[int]], out: TextIOBase) -> None:
    str_triangle = list(map(lambda row: list(map(lambda i: str(i), row)), triangle))
    if len(str_triangle) <= 1:
        for line in str_triangle:
            out.write(line[0])
            out.write("\n")
    else:
        # Good position for the midpoints of the numbers of the last line,
        # measured in units of 1/4 of character width.
        last_line_midpoints = []
        current_position_in_line = -4 # Compensate for the unneeded initial blank.
        for s in str_triangle[-1]:
            current_position_in_line += 4  # one blank
            last_line_midpoints.append(current_position_in_line + 2 * len(s))
            current_position_in_line += 4 * len(s)

        # Same for not the last, but the previous-to-the-last line.
        # Try to put midpoints exactly between those of the last line.
        next_to_last_line_midpoints = []
        for i in range(len(str_triangle[-2])):
            left_below = last_line_midpoints[i]
            right_below = last_line_midpoints[i + 1]
            next_to_last_line_midpoints.append((left_below + right_below) // 2)

        # Use such data to position any line above the midpoints.
        # We position either according to the midpoints of the last line
        # or according to the midpoints of the previous to last line,
        # according to our line being odd or even.
        def position_above_midpoints(
            line_to_print: list[str],
            midpoint_positions: list[int],
            skip_initial_midpoints: int,
        ) -> None:
            current_position_in_line = 0
            for str_index in range(len(line_to_print)):
                str_to_print = line_to_print[str_index]
                wanted_midpoint = midpoint_positions[skip_initial_midpoints + str_index]
                wanted_startpoint = wanted_midpoint - 2 * len(str_to_print)
                initial_space_needed = wanted_startpoint - current_position_in_line
                out.write(" " * (initial_space_needed // 4))
                current_position_in_line += (initial_space_needed // 4) * 4
                if initial_space_needed % 4 >= 2:
                    out.write(" ")
                    current_position_in_line += 4
                out.write(str_to_print)
                current_position_in_line += 4 * len(str_to_print)
            out.write("\n")

        for line_index in range(len(str_triangle)):
            line_to_print = str_triangle[line_index]
            if (len(str_triangle) - line_index) % 2 == 1:
                # line_to_print items should be above last_line items
                position_above_midpoints(
                    line_to_print,
                    last_line_midpoints,
                    (len(str_triangle) - line_index) // 2,
                )
            else:
                # line_to_print items should be above next_to_last_line items
                position_above_midpoints(
                    line_to_print,
                    next_to_last_line_midpoints,
                    (len(str_triangle) - 1 - line_index) // 2,
                )


if __name__ == "__main__":
    from argparse import ArgumentParser

    parser = ArgumentParser(
        prog="pascal_dreieck.py",
        description="Print the top of Pascal's triangle to stdout",
    )
    parser.add_argument("lines", type=int, help="The number of lines wanted.")
    args = parser.parse_args()
    triangle = generate_pascal_triangle(args.lines)
    format_pascal_triangle(triangle, cast(TextIOBase, stdout))

Wer dies diskutieren möchte und einen Fediverse-Zugang hat, ist eingeladen, diesen Tröt zu kommentieren.