@higgins1a: Namen sollten nicht egal sein. Wegwerfskripte haben oft die Angewohnheit nicht sterben zu wollen und man löscht die ja auch meistens nicht. Und dann kommt in ein oder zwei Jahren wieder ein PDF, welches genau so oder so ähnlich aussieht, und man erinnert sich das man da ja schon irgendwas hatte, und schon muss man sich in etwas mit schlechten Namen einarbeiten. Was einfacher wäre, wenn man dann nicht überlegen müsste was die Namen bedeuten, oder noch schlimmer wenn man keine Fehler machen würde, weil man denkt man weiss was ein Name bedeutet, aber falsch liegt.
Zumal es auch wenig Sinn macht Python lernen zu wollen, aber dann nicht auch die Namenskonventionen zu lernen. Was man auch in echten Wegwerfskripten üben kann.
Zumindest ein bisschen besser wäre es wenn man die Endzeile nicht nur an der Anzahl der Punkte erkennt, sondern mindestens einen regulären Ausdruck verwendet, der die Datumsangaben parst. Noch besser wenn man dann danach noch überprüft, ob es zusammen mit der Jahreszahl auch in beiden Fällen ein gültiges Datum ist, also da beispielsweise nicht der '29.02.2017' steht, was man mit regulären Ausdrücken alleine ja nicht erkennen kann. Das kann man in Python sehr einfach mit dem `datetime`-Modul machen, in dem man einfach versucht das Datum in ein `datetime.datetime`-Objekt zu parsen.
Noch mal zum `numberElements()` wie man das in Python machen würde, wenn Zeichenketten (und andere Sequenztypen) dafür nicht bereits eine Methode hätten. Deine Funktion mit besseren Namen, nicht nur was die Schreibweise angeht, sondern auch generischer, denn das funktioniert mit allen Sequenztypen und nicht nur mit Zeichenketten:
Code: Alles auswählen
def item_count(sequence, needle):
result = 0
for i in range(len(sequence)):
if sequence[i] == needle:
result += 1
return result
Ich habe das `item_count()` statt `element_count()` genannt, weil `item` in Python wesentlich üblicher als `element` ist, sowohl in der Dokumentation als auch in der API. Das wäre ein Beispiel warum Namensgebung auch zum lernen einer Sprache gehört.
Die Funktion ist ”unpythonisch” wegen dem unnötigen Laufindex. Gerade wenn man von Sprachen kommt in denen viel mit Indexwerten hantiert wird, muss man in Python umdenken, denn da geht es fast immer ohne Index, und sollte man einen Index oder eine laufende Zahl *zusätzlich* zum Element in einer Iteration benötigen, dann gibt es dafür die `enumerate()`-Funktion. Ohne Laufindex:
Code: Alles auswählen
def item_count(iterable, needle):
result = 0
for item in iterable:
if item == needle:
result += 1
return result
Hier konnte ich jetzt `sequence` in `iterable` umbenennen weil das Argument keine Sequenz mehr sein muss, sondern ein beliebiges iterierbares Objekt sein kann. Einzige praktische Einschränkung: Es sollte ein endlich iterierbares Objekt sein, sonst wartet man *sehr* lange auf das Ergebnis.

Auch hier ist ein überlegen bezüglich des Namens wieder nicht ganz unwichtig beim lernen der Sprache.
Mit der `sum()`-Funktion und einem Generatorausdruck kann man das nun auf eine Zeile zusammenschrumpfen lassen:
Code: Alles auswählen
def item_count(iterable, needle):
return sum((1 if item == needle else 0) for item in iterable)
Da `bool` von `int` abgeleitet ist und `True` als ganze Zahl den Wert 1 und `False` den Wert 0 hat, kann das sogar noch ein kleines bisschen verkürzen:
Code: Alles auswählen
def item_count(iterable, needle):
return sum(item == needle for item in iterable)
Aber wie schon gesagt: Sequenztypen, und damit auch Zeichenketten, haben dafür schon eine Methode.
Auch in einem Wegwerfskript würde ich die beiden Zustände (ausserhalb einer Kontobewegung und innerhalb einer Kontobewegung) im Code deutlich trennen. Deine drei ``if``-Bedingungen funktionieren zwar, aber das das so ist, muss man erst überlegen.
Und ich würde hier möglichst defensiv programmieren. Io hat nichts für CSV-Dateien in der Standardbibliothek, aber ich habe nicht einfach nur einen `join()` mit dem Feldtrenner gemacht, sondern ich teste vorher ob der Feldtrenner oder Anführungszeichen in den Werten vorkommen und löse dann eine Ausnahme aus, damit so etwas nicht lautlos durchrutscht und die kaputten Daten erst später Probleme bereiten. Aber wie auch schon mehrfach gesagt: In Python gibt es schon etwas für CSV-Dateien in der Standardbibliothek was mit Trennern und Anführungszeichen in Feldern klar kommt.
Hier ist die Io-Funktion, die nach dem Diagramm entstanden ist:
[codebox=io file=Unbenannt.txt]parseAccountActivitiesFile := method(fileName, year,
rows := List clone
parseDate := method(string, Date fromString(string .. year, dateFormat))
file := fileName asFile openForReading
error := try(
amount := nil
reasonLines := nil
file foreachLine(i, line,
if(amount isNil) then(
startLineMatch := line findRegex(amountRegex)
if(startLineMatch) then(
amount = startLineMatch at(1) .. startLineMatch at(2)
reasonLines = List clone
)
// Lines between account activities are ignored here.
) else(
endLineMatch := line findRegex(datesRegex)
if(endLineMatch) then(
dateA := parseDate(endLineMatch at(1))
dateB := parseDate(endLineMatch at(2))
if((dateA asNumber != -1) and(dateB asNumber != -1)) then(
rows append(
List with(
amount,
reasonLines join(" "),
dateA asString(dateFormat),
dateB asString(dateFormat)
)
)
amount = nil
reasonLines = nil
)
) else(
reasonLines append(line strip)
)
)
)
)
file close
error pass
amount ifNonNil(ParseError raise("missing end line at " .. (i + 1)))
rows
)[/code]
Das ist schon ziemlich lang für *eine* Funktion. IMHO schon zu lang. Aber man könnte das fast 1:1 in Python umschreiben.
`amount` und `reasonLines` gehören eigentlich eng zusammen wie man im Code sehen kann. Und `dateA` und `dateB` gehören da eigentlich auch dazu, was man im Code nicht so gut sehen kann. Also mindestens die Werte, die zu einer Kontobewegung gehören, würde ich in einer `AccountActivity` Klasse zusammenfassen.
Ich habe noch eine Lösung in CoffeeScript geschrieben, da sieht die `AccountActivity`-Klasse zum Sammeln der Daten für eine Kontobewegung so aus:
[codebox=coffeescript file=Unbenannt.coffee]class AccountActivity
@DATE_FORMAT: 'DD.MM.YYYY'
@parseDate: (string) => new moment string, @DATE_FORMAT
constructor: (@amount, @reasonLines = [], @dateA, @dateB) ->
addReasonLine: (line) => @reasonLines.push line
asRow: => [
@amount,
@reasonLines.join(' '),
@dateA.format(@constructor.DATE_FORMAT),
@dateB.format(@constructor.DATE_FORMAT),
][/code]
Und `parseAccountActivitiesFile()` aus dem Io-Programm ist in der CoffeeScript-Lösung in einer `AccountActivityParser`-Klasse auf mehrere Methoden verteilt. Es gibt zwar auch bei Node.js nichts für CSV-Dateien in der Standardbibliothek, aber mit ``npm`` ist schnell etwas installiert. `csv-fast` in dem Fall. Und es gibt mit `argparse` eine Portierung vom `argparse`-Modul aus der Python-Standardbibliothek. Das CoffeeScript-Programm hat also eine nette Kommandozeilen-API mit generierter Hilfe:
[codebox=text file=Unbenannt.txt]$ ./test.coffee
usage: test.coffee [-h] [-v] filename [year]
test.coffee: error: too few arguments
$ ./test.coffee -h
usage: test.coffee [-h] [-v] filename [year]
Convert account activity text file to CSV file.
Positional arguments:
filename filename of the text file with account activities.
year year of dates in the text file. (default: current year)
Optional arguments:
-h, --help Show this help message and exit.
-v, --version Show program's version number and exit.
$ ./test.coffee --version
0.0.1[/code]