Design- Frage: Excel Umrechnung 'F1:H2' --> (5,7,2,1)

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Benutzeravatar
NoPy
User
Beiträge: 123
Registriert: Samstag 28. Dezember 2013, 12:39

Freitag 23. Oktober 2015, 07:46

Das Thema etwas genauer:

ich will in irgendeiner Form mit Excel spielen.

ich benötige etwas, das aus
- C1 irgendwie zeile 0, spalte 2
- C1:F2 irgendwie von_zeile 0, von_spalte 2, von_zeile 1, bis_spalte 5
- C:C
und umgekehrt

klassisch wäre etwas in der Form

Code: Alles auswählen

def berechne_zellenkoordinaten(eine_zelle):
    if eine_zelle == 'C1':
        return {'spalte':2;'zeile':0}

def berechne_bereichskoordinaten(ein_bereich):
    if ein_bereich == 'C1:F2':
        von = berechne_zellenkoordinaten('C1')
        bis = berechne_zellenkoordinaten('F2')
        return {'von_spalte':von['spalte'], 'bis_spalte':bis['spalte'], 'von_zeile':von['zeile'], 'bis_zeile':bis['zeile']}

def berechne_bereichstext_aus_woerterbuch(eine_zelle):
    if eine_zelle=={'spalte':2;'zeile':0}:
        return 'C1'

def berechne_zellentext_aus_liste(spalte,zeile):
    if spalte ==2 and zeile==0:
        return 'C1'
usw.
Das ist natürlich hinreichend eklig.

besser erscheint mir irgendetwas der Form

Code: Alles auswählen

class excel_umrechnung(objekt):
    def __init__(self,ein_bereich):
        if ein_bereich == 'C1:F2':
            self.koordinaten = {'von_spalte':2, 'bis_spalte':5, 'von_zeile':0, 'bis_zeile':1}
        elif ein_bereich == {'von_spalte':2, 'bis_spalte':5, 'von_zeile':0, 'bis_zeile':1}:
            self.koordinaten = {'von_spalte':2, 'bis_spalte':5, 'von_zeile':0, 'bis_zeile':1}

    def als_string(self):
        return 'C1:F2'

    def als_woerterbuch(self):
        return {'von_spalte':2, 'bis_spalte':5, 'von_zeile':0, 'bis_zeile':1}

    def als_liste(self):
        return [2,0,5,1]
Aber wie macht man das nun "richtig"?
Sirius3
User
Beiträge: 8791
Registriert: Sonntag 21. Oktober 2012, 17:20

Freitag 23. Oktober 2015, 08:27

@NoPy: sauber wären zwei Klassen

Code: Alles auswählen

import re
from collections import namedtuple

class ExcelCoordinate(namedtuple("ExcelCoordinate", "column, row")):
    @classmethod
    def from_string(cls, cell):
        cell = re.match('^([A-Za-z]+)(\d+)$', cell)
        if cell is None:
            raise ValueError("no cell value")
        return cls(abc_to_int(cell.group(1)), int(cell.group(2)))
    
    def __str__(self):
        return '%s%d' %(int_to_abc(self.column), self.row)

class ExcelArea(namedtuple("ExcelArea", "upper_left, lower_right")):
    @classmethod
    def from_string(cls, area):
        ul, lr = area.split(':')
        return cls(ExcelCoordinate.from_string(ul),ExcelCoordinate.from_string(lr))
    
    def __str__(self):
        return '%s:%s' %(self.ul, self.lr)
Man teilt also das Problem in kleinere Teile, ein Bereich besteht aus zwei Koordinaten, und löst jedes unabhängig. Schnittstellen werden eindeutig definiert, so dass es keine Gott-Funktionen gibt, die mit allem umgehen können (müssen), sondern ich als Anwender der Klassen weiß ja, ob ich mit einem String oder schon mit Zahlentupeln arbeite.
Benutzeravatar
NoPy
User
Beiträge: 123
Registriert: Samstag 28. Dezember 2013, 12:39

Freitag 23. Oktober 2015, 09:22

die eigentliche Umrechnungslogik steckt dann in abc_to_int bzw. int_to_abc

Und wie stelle ich mir dann einen Aufruf vor?

Code: Alles auswählen

k=ExcelCoordinate('C1')
print k['column'],k['row']
Wie baue ich dann Bereiche ein? Mache ich "from_string" intelligenter

cell = re.match('^([A-Za-z][A-Za-z]?)(\d+)([:]([A-Za-z][A-Za-z]?)(\d+))?$', cell)
oder
cell_einzeln = re.match('^([A-Za-z]+)(\d+)$', cell)
cell_bereich = re.match('^([A-Za-z]+)(\d+)[:]([A-Za-z]+)(\d+)$', cell)
cell_spalten = re.match('^([A-Za-z]+)[:]([A-Za-z]+)$', cell)
cell_zeilen = re.match('^(\d+)[:](\d+)$', cell)
?

An welcher Stelle sollte man dann prüfen, dass 'E:A' oder 'C4:D2' nicht geht?
Absolute Positionierung '$A$2:C$5' (also mir geht es nicht um den regulären Ausdruck, den bekomme ich hin), wie/wo bringe ich elegant unter, dass eine von- Spalte absolut und die bis- Spalte relativ ist?
Benutzeravatar
pillmuncher
User
Beiträge: 1118
Registriert: Samstag 21. März 2009, 22:59
Wohnort: München

Freitag 23. Oktober 2015, 09:35

@NoPy: Die besten Erfahrungen mit Python + Excel habe ich gemacht, wenn ich win32com verwendet habe, um Excel fernzusteuern. Hier eine Kurzanleitung: http://pythonexcels.com/python-excel-mini-cookbook/

Dein Problem verschwindet damit insofern, als das Umrechnen von Zellenadressen direkt von Excel selbst erledigt wird, und Excel, so steht anzunehmen, kann das einigermaßen gut.

Funktioniert allerdings nur unter Windows.
In specifications, Murphy's Law supersedes Ohm's.
Benutzeravatar
NoPy
User
Beiträge: 123
Registriert: Samstag 28. Dezember 2013, 12:39

Freitag 23. Oktober 2015, 09:51

pillmuncher hat geschrieben:@NoPy: Die besten Erfahrungen mit Python + Excel habe ich gemacht, wenn ich win32com verwendet habe, um Excel fernzusteuern. Hier eine Kurzanleitung: http://pythonexcels.com/python-excel-mini-cookbook/

Dein Problem verschwindet damit insofern, als das Umrechnen von Zellenadressen direkt von Excel selbst erledigt wird, und Excel, so steht anzunehmen, kann das einigermaßen gut.

Funktioniert allerdings nur unter Windows.
Ich weiß, das hatte ich auch schon überlegt. Aber das setzt nicht nur Windows voraus, sondern auch Excel. Und diesbezüglich wollte ich es flach halten, da u.U. auch mal nötig ist, dass ich das an OpenOffice anpassen muss oder irgendwelche Software auf einem Server laufen lassen muss, auf dem Windows oder Excel nicht installiert ist.

Üblicherweise können alle Kollegen so leidlich mit Excel umgehen, so dass ein Austausch von Informationen auf diesem Wege immer funktioniert, daher bin ich daran interessiert, eine zukunftsfähige Schnittstelle zu Excel- Dateien zu etablieren.
Sirius3
User
Beiträge: 8791
Registriert: Sonntag 21. Oktober 2012, 17:20

Freitag 23. Oktober 2015, 10:27

@NoPy: ich hatte gedacht, es geht Dir ums Konzept und nicht um die Umsetzung.
Wie schon geschrieben, sollte eine Funktion ein einfaches Interface haben und man macht lieber zwei Funktionen, wenn der Input verschieden ist. In diesem Fall heißt das:

Code: Alles auswählen

k = ExcelCoordinate.from_string('C1')
b = ExcelCoordinate(3,1)
assert k.column = b.column
Für "absolut" brauchst Du halt noch weitere Attribute row_fixed, column_fixed. Mehr Funktionalität bedeutet komplexere Klassen.
Wenn Du bliebige Excel-Zell-Range-Beschreibungen parsen willst, würde ich das in eine eigene Funktion auslagern, bzw. ExcelArea den Spezialfall, dass upper-left = lower-right ist, beibringen.
Wie man prüft, ob der reguläre Ausdruck passt, habe ich Dir ja schon gezeigt.
Benutzeravatar
NoPy
User
Beiträge: 123
Registriert: Samstag 28. Dezember 2013, 12:39

Freitag 23. Oktober 2015, 10:59

Sirius3 hat geschrieben:@NoPy: ich hatte gedacht, es geht Dir ums Konzept und nicht um die Umsetzung.
Oh, darum geht es mir immer noch. Deiner Antwort habe ich entnommen, dass Du im Grunde bei diesen beiden Klassen bleiben und diesen letztlich mehr Parameter spendieren würdest.
Sirius3 hat geschrieben:Wie schon geschrieben, sollte eine Funktion ein einfaches Interface haben und man macht lieber zwei Funktionen, wenn der Input verschieden ist. In diesem Fall heißt das:

Code: Alles auswählen

k = ExcelCoordinate.from_string('C1')
b = ExcelCoordinate(3,1)
assert k.column = b.column
Für "absolut" brauchst Du halt noch weitere Attribute row_fixed, column_fixed. Mehr Funktionalität bedeutet komplexere Klassen.
Wenn Du bliebige Excel-Zell-Range-Beschreibungen parsen willst, würde ich das in eine eigene Funktion auslagern, bzw. ExcelArea den Spezialfall, dass upper-left = lower-right ist, beibringen.
Wie man prüft, ob der reguläre Ausdruck passt, habe ich Dir ja schon gezeigt.
Das mit den regulären Ausdrücken ist kein Problem, damit hantiere ich seit Jahren.
Die Schnittstelle nach außen sollte schon so klein wie möglich sein. So sieht das schon mal gut aus.
An welcher Stelle würdest Du die Fallunterscheidung unterbringen?
- in der "Hilfsfunktion", die die die eigentliche Berechnung macht? (abc_to_int)
- in "from_string" a la if ... elif ...
- noch anders?
Ich gehe mal davon aus, dass alle von einer Klassenmethode aufgerufenen Methoden auch Klassenmethoden sein müssen, da je kein Objekt existiert
Sirius3
User
Beiträge: 8791
Registriert: Sonntag 21. Oktober 2012, 17:20

Freitag 23. Oktober 2015, 14:23

@NoPy: Konkret ist immer besser, als groß Fallunterscheidungen zu machen. Sehe jetzt auch nicht, wo man da if..elif.. braucht.
Liffi
User
Beiträge: 153
Registriert: Montag 1. Januar 2007, 17:23

Freitag 23. Oktober 2015, 15:44

Vielleicht hilft es ja schon, dass 'echte' Format zu benutzen, welches Excel auch intern nutzt. Mit einer Deutschen Local wird dann aus C1 Z1S3, damit sollte man schon mal einfacher arbeiten können.
Benutzeravatar
NoPy
User
Beiträge: 123
Registriert: Samstag 28. Dezember 2013, 12:39

Freitag 23. Oktober 2015, 17:51

Sirius3 hat geschrieben:@NoPy: Konkret ist immer besser, als groß Fallunterscheidungen zu machen. Sehe jetzt auch nicht, wo man da if..elif.. braucht.
na um zu unterscheiden zwischen C1, C1:D2, C$1, $C1, $C$1, C:C, $C:C ...
Vielleicht kann man auch notfalls einen universellen regulären Ausdruck schreiben und dann - je nachdem, was in den Gruppen steht - füllen.
Aber ist das besser?

Code: Alles auswählen

// AB   b              a CD   d    cE   FG   g              f HI   i    he
 '^(([$])?[A-Za-z]{1,2})?(([$])?\d+)([:](([$])?[A-Za-z]{1,2})?(([$])?\d+))?$'
(zur Erklärung: Ich hab mir angewöhnt, öffnende Klammern mit Groß- und schließende mit Kleinbuchstaben zu markieren. Wartet sich leichter)
spalte_von_absolut = group(1)='$'
spalte_von = group(0) (wobei man noch das evtl. vorhandene $ entfernen muss, entweder, indem man den regulären Ausdruck noch weiter aufbohrt, oder indem man eine Fallunterscheidung macht.

Aber dennoch müsste man auch noch prüfen, ob das Ergebnis valide ist, denn A:3 frißt zwar der reguläre Ausdruck, nicht aber Excel.
Also an irgend einer Stelle müssen Fälle unterschieden werden.
Liffi hat geschrieben:Vielleicht hilft es ja schon, dass 'echte' Format zu benutzen, welches Excel auch intern nutzt. Mit einer Deutschen Local wird dann aus C1 Z1S3, damit sollte man schon mal einfacher arbeiten können.
Das umrechnen ist zwar nicht unkomplex, aber auch nicht unmöglich. Mit Z1S3 kann openpyxl - zumindest, soweit ich das gesehen habe - nicht umgehen.
Benutzeravatar
NoPy
User
Beiträge: 123
Registriert: Samstag 28. Dezember 2013, 12:39

Freitag 23. Oktober 2015, 21:18

Jetzt verlasse ich mal die Frage nach dem Design kurz:

Mit

Code: Alles auswählen

//        AB   b C             ca DE   e F   fd G   HI   i J             jh KL   l M   mk g
lvsRE = '^(([$])?([A-Za-z]{1,2}))?(([$])?(\d+))?([:](([$])?([A-Za-z]{1,2}))?(([$])?(\d+))?)?$'
kann ich im Grunde jeden Fall abbilden, in den Gruppen 2,5,9 und 12 stecken die Dollars drin, in 3 und 10 die Buchstaben, in 6 und 13 die Zahlen.

Eine Klasse, die von named_tupel erbt (das Konzept named_tupel habe ich noch nicht verstanden), sollte jetzt im Grunde irgendetwas in der Form

Code: Alles auswählen

    cls.von_spalte = abc_to_int(cell.group(3))
    cls.von_spalte_fix = cell.group(2)=='$'
    cls.von_zeile = int(cell.group(6))
    cls.von_zeile_fix = cell.group(5)=='$'
    ...
    return cls
machen, oder? Wie geht das technisch in oben beschriebener Konstruktion, vorausgesetzt natürlich, die Namen sind in der Tupel- Definition drin.

Code: Alles auswählen

class ExcelCoordinate(namedtuple("ExcelCoordinate", "von_spalte, von_spalte_fix, von_zeile,von_zeile_fix")):
    @classmethod
    def from_string(cls, cell):
        cell = re.match('^(([$])?([A-Za-z]{1,2}))?(([$])?(\d+))?([:](([$])?([A-Za-z]{1,2}))?(([$])?(\d+))?)?$', cell)
        if cell is None:
            raise ValueError("no cell value")
        else:
            cls.von_spalte = abc_to_int(cell.group(3))
            cls.von_spalte_fix = cell.group(2)=='$'
            #...
            if ist_gueltig(cls):
                return cls
            else:
                raise ValueError("no cell value")
Welchen Vorteil hat die Implementation als Klasse/Klassenmethode an dieser Stelle? Könnte man dann nicht genauso eine 08/15- Funktion benutzen? Wenn diese ein named tupel zurückwirft, dann stellt sich das für den Aufrufenden genauso dar, oder? (Keine Kritik, nur eine Frage zum Verständnis)
Sirius3
User
Beiträge: 8791
Registriert: Sonntag 21. Oktober 2012, 17:20

Samstag 24. Oktober 2015, 19:29

@NoPy: wenn Du das als Klassenfunktion schreibst, dann kannst Du die Klasse auch ableiten, wenn es nötig wird. Aber sonst kannst Du natürlich auch eine normale Funktion schreiben. Wie gesagt, ich würde das Problem aufspalten in eine einzelne Koordinate und einem Range-Objekt. Bei "ExcelCoordinate" macht das 'von_'-Präfix irgendwie keinen Sinn.
Benutzeravatar
snafu
User
Beiträge: 5631
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Samstag 24. Oktober 2015, 20:30

NoPy hat geschrieben:Jetzt verlasse ich mal die Frage nach dem Design kurz:

Mit

Code: Alles auswählen

//        AB   b C             ca DE   e F   fd G   HI   i J             jh KL   l M   mk g
lvsRE = '^(([$])?([A-Za-z]{1,2}))?(([$])?(\d+))?([:](([$])?([A-Za-z]{1,2}))?(([$])?(\d+))?)?$'
kann ich im Grunde jeden Fall abbilden, in den Gruppen 2,5,9 und 12 stecken die Dollars drin, in 3 und 10 die Buchstaben, in 6 und 13 die Zahlen.

Eine Klasse, die von named_tupel erbt (das Konzept named_tupel habe ich noch nicht verstanden), sollte jetzt im Grunde irgendetwas in der Form

Code: Alles auswählen

    cls.von_spalte = abc_to_int(cell.group(3))
    cls.von_spalte_fix = cell.group(2)=='$'
    cls.von_zeile = int(cell.group(6))
    cls.von_zeile_fix = cell.group(5)=='$'
    ...
    return cls
machen, oder?
Naja, das wäre wohl ziemlich schlecht wartbar. Inbesondere falls jemand anderes als du mal mit dem Code zu tun hat.

Ich würde den komplexen regulären Ausdruck eher in Teilausdrücke zerlegen und mitels passender Hilfsfunktionen direkt bei Erzeugung des Klassenexemplars die Teilausdrücke durchtesten. Die Hilfsfunktionen würden in etwa so aussehen:

Code: Alles auswählen

def has_pattern(s, pattern):
    return bool(re.search(pattern, s))

def get_int(s, pattern):
    match = re.search(pattern, s)
    if not match:
        raise ValueError('no match')
    return int(match.group(0))
shcol (Repo | Doc | PyPi)
Benutzeravatar
cofi
Moderator
Beiträge: 4432
Registriert: Sonntag 30. März 2008, 04:16
Wohnort: RGFybXN0YWR0

Sonntag 25. Oktober 2015, 00:20

Ich wuerde noch named groups und re.VERBOSE vorschlagen um das Monster zu baendigen.
Benutzeravatar
pillmuncher
User
Beiträge: 1118
Registriert: Samstag 21. März 2009, 22:59
Wohnort: München

Sonntag 25. Oktober 2015, 03:20

Man kann sich auch Parserkombinatoren bauen:

Code: Alles auswählen

import re
import pprint


class Parser:

    def _parse(self, result, text):
        raise NotImplementedError

    def parse(self, text):
        result, rest = self._parse([], text)
        if rest:
            raise ValueError('Found trailing string: {}'.format(rest))
        else:
            return result

    def __add__(self, parser):
        return Then(self, parser)

    def __or__(self, parser):
        return Either(self, parser)


class Then(Parser):

    def __init__(self, first, second):
        self.first = first
        self.second = second

    def _parse(self, result, text):
        return self.second._parse(*self.first._parse(result, text))


class Either(Parser):

    def __init__(self, first, second):
        self.first = first
        self.second = second

    def _parse(self, result, text):
        try:
            return self.first._parse(result, text)
        except ValueError:
            return self.second._parse(result, text)


class Optional(Parser):

    def __init__(self, parser):
        self.parser = parser

    def _parse(self, result, text):
        try:
            return self.parser._parse(result, text)
        except ValueError:
            return result, text


class RegexParser(Parser):

    def __init__(self):
        self.match = re.compile(pattern=self.pattern).match

    def _parse(self, result, text):
        match = re.match(self.pattern, text)
        if match:
            return (
                result + [{type(self).__name__: match.group()}],
                text[match.end():],
            )
        else:
            raise ValueError


class Column(RegexParser):
    pattern = '[A-Z]+'


class Row(RegexParser):
    pattern = '\d+'


class Colon(RegexParser):
    pattern = ':'


class Dollar(RegexParser):
    pattern = '\$'


EXCEL_ADDRESS = (
    Optional(Dollar())
    + Column()
    + Optional(Dollar())
    + Row()
    + Optional(Colon()
               + Optional(Dollar())
               + Column()
               + Optional(Dollar())
               + Row())
    |
    Optional(Dollar())
    + Column()
    + Colon()
    + Optional(Dollar())
    + Column()
    |
    Optional(Dollar())
    + Row()
    + Colon()
    + Optional(Dollar())
    + Row()
)


def parse(addr):
    return EXCEL_ADDRESS.parse(addr)


def show(addr):
    print(addr)
    pprint.pprint(parse(addr))
    print()


def main():
    show('A1')
    show('$A1')
    show('A$1')
    show('$A$1')
    show('A1:G7')
    show('$A$1:$G$7')
    show('$A:$G')
    show('1:1')


if __name__ == '__main__':
    main()
Ergebnis:

Code: Alles auswählen

A1
[{'Column': 'A'}, {'Row': '1'}]

$A1
[{'Dollar': '$'}, {'Column': 'A'}, {'Row': '1'}]

A$1
[{'Column': 'A'}, {'Dollar': '$'}, {'Row': '1'}]

$A$1
[{'Dollar': '$'}, {'Column': 'A'}, {'Dollar': '$'}, {'Row': '1'}]

A1:G7
[{'Column': 'A'}, {'Row': '1'}, {'Colon': ':'}, {'Column': 'G'}, {'Row': '7'}]

$A$1:$G$7
[{'Dollar': '$'},
 {'Column': 'A'},
 {'Dollar': '$'},
 {'Row': '1'},
 {'Colon': ':'},
 {'Dollar': '$'},
 {'Column': 'G'},
 {'Dollar': '$'},
 {'Row': '7'}]

$A:$G
[{'Dollar': '$'},
 {'Column': 'A'},
 {'Colon': ':'},
 {'Dollar': '$'},
 {'Column': 'G'}]

1:1
[{'Row': '1'}, {'Colon': ':'}, {'Row': '1'}]
Das ganze ist nur eine schematische Darstellung, wie man sowas bauen könnte.

Parserkombinatoren können - anders als reguläre Ausdrücke - auch kontextbehaftete Grammatiken parsen und es ist leicht, die Klassen um semantische Prüfungen zu erweitern, damit zB. "G10:A1" ungültig ist. Dazu sollte man natürlich eine weniger naive Ergebnisstruktur aufbauen, die man dann während des Parsens lesen und manipulieren kann.

Noch besser, als die Prüfungen in die Klassen einzubauen, ist es, monadische Bind- und Return-Parser zu verwenden:

Code: Alles auswählen

class Bind(Parser):

    def __init__(self, parser, function):
        self.parser = parser
        self.function = function

    def _parse(self, result, text):
        r, t = self.parser._parse(result, text)
        return self.function(r)._parse(r, t)


class Return(Parser):

    def __init__(self, value=None):
        self.value = value

    def _parse(self, result, text):
        if self.value is None:
            return result, text
        else:
            return result + [self.value], text
Der Return-Parser fügt einfach einen vordefinierten (nicht-None-) Wert zum Ergebnis dazu. Der Bind-Parser ist etwas komplizierter: er verwendet einen Parser, dessen Ergebnis dann an eine Funktion übergeben wird, die wieder einen Parser produziert, der dann den restlichen Text weiterparst. Damit kann man während des Parsens dynamisch die zugrundeliegende Grammatik ändern. Oder eben semantische Prüfungen einbauen:

Code: Alles auswählen

def check_rows_are_monotonic(result):
    result = iter(result)
    for token in result:
        if 'Row' in token:
            start_row = int(token['Row'])
            break
    for token in result:
        if 'Row' in token:
            end_row = int(token['Row'])
            break
    if start_row > end_row:
        raise ValueError('Wrong order of row indices!')
    else:
        return Return()

EXCEL_ADDRESS = (
    ...  # like before
    |
    Optional(Dollar())
    + Row()
    + Colon()
    + Optional(Dollar())
    + Bind(Row(), check_rows_are_monotonic)

...

show('2:1')
Ergebnis:

Code: Alles auswählen

Traceback (most recent call last):
...
  File "exad.py", line 32, in _parse
    return self.function(r)._parse(r, t)
  File "exad.py", line 122, in check_rows_are_monotonic
    raise ValueError('Wrong order of row indices!')
ValueError: Wrong order of row indices!
Mein Code ist allerdings nicht ganz korrekt. Bind sollte statt des gesamten Ergebnisses nur das Ergebnis des Parsers übergeben, den es kapselt. Dazu müsste ich allerdings die gesamte Implementation ändern und das ist mir um diese Uhrzeit zu viel Arbeit. Damit Bind Zugriff auf Start- und Endzeile hat, müsste es dann so aussehen:

Code: Alles auswählen

EXCEL_ADDRESS = (
    ...  # like before
    |
    Bind(
        Optional(Dollar())
        + Row()
        + Colon()
        + Optional(Dollar())
        + Row(), check_rows_are_monotonic)
In specifications, Murphy's Law supersedes Ohm's.
Antworten