Skip to content

Instantly share code, notes, and snippets.

@juvuorin
Created April 28, 2021 08:48
Show Gist options
  • Save juvuorin/5bdfeef7e2ed1df7ea3e7d36e4d0cacb to your computer and use it in GitHub Desktop.
Save juvuorin/5bdfeef7e2ed1df7ea3e7d36e4d0cacb to your computer and use it in GitHub Desktop.
Koiramainen ohjelmointikisa 2021
# Alla oleva koodirivi on yhden rivin Python-toteutus Ideal Learningin koiramaiseen ohjelmointikisaan (vol.2).
# Toteutukseen on tehty yhdelle riville juuri siksi, että Python normaalisti lohkottaa koodin sisennysten
# avulla, mikä tekee yhden rivin koodeista mukavia pulmapähkinöitä (olettaen, että puolipistettä ei saa käyttää).
# Tänä vuonna pääsin hyödyntämään ratkaisussa muutamia tekniikoita jotka viime kerralla päätin jättää pois, mutta
# valitettavasti tämän vuoden ongelmaan ei päässyt kirjoittamaan populaarikulttuuriviittauksia, sillä ratkaisussa
# ei ole tarpeettomia lohkoja.
# Tämänkertainen tehtävänanto oli varsin laajatulkintainen, ja päätin luoda kirjauksia varten seuraavia sääntöjä:
# * Syötteen rivit ovat pilkuilla eroteltuja arvoja, desimaalierottimena käytetään pistettä
# * Syöte alkaa aina rivillä "Tilinumero,Rahamäärä"
# * Tilinumerot ja rahamäärät ovat positiivisia kokonaislukuja. Lukujen alussa olevia nollia ei kirjoiteta tuotokseen.
# * Muutama määrätty tili, sekä joitakin tilinumeroiden rajoituksia:
# * 0700: liiketoiminnan kokonaismenot
# * 1700: liiketoiminnan kokonaistulot
# * 2700: ALV
# * 3700: Osinkosaldo
# * 4700: Napostelukassa
#
# * XX10: ALVin alaiset tuotot
# * XX20: Firman saamat lahjoitukset
# * XX30: Poistoista saatavat tuotot/vähennykset
# * XX40: Napostelumenot
# * XX50: Terveydenhuollon kulut
# * XX60: Järsittyjen huonekalujen uusintakulut
# * XX70: Osinkojen maksut
#
# * Tuotoksessa olevien kirjauksien rahamäärät voivat olla myös negatiivisia.
# * Jos tilinumero ei mene mihinkään listatuista kategorioista, se oletetaan pöydän alta saaduksi tuotoksi (joskin
# kirjuri-koiro on tunnollisesti tehnyt siitä tapahtuman). Automaatio ei tee tapahtumalle ylimääräisiä toimenpiteitä.
#
# Yhtiön eri tileillä olevaa rahamäärää ei varsinaisesti pidetä mitenkään kirjattuna, ja osinkosaldon ja napostelukassan
# plussalla pysyminen jätetään kirjanpitäjän vastuulle.
# Koodini hyödyntää list comprehensioneita, lambda-funktioita, ternäärisiä if-rakenteita, sekä ah-niin-ihanaa
# lambda-rekursiota! Onnistuin myös eliminoimaan enimmät heikkotehoiset ratkaisut lopullisesta koodista.
# Ohjelmalle annetaan parametreiksi kahden tiedoston nimet, jotka ovat oletuksena "syote.txt" ja "tuotos.txt".
# Syötetiedostosta luetaan ensin otsikkorivi, minkä jälkeen kukin tapahtumarivi käsitellään yksi kerrallaan. Jokaisen
# tapahtuman kohdalla tarkastetaan tilinumeron muoto, ja tehdään mahdollisia automaatiotapahtumia. Kun kaikki syötteen
# tapahtumat on käyty läpi, alustettuun tuotos-tiedostoon kirjoitetaan käsitelty tapahtumalista.
# Automaattisten vientien säännöt on toteutettu valitettavan tylsällä if-else -ketjulla. Tilinumerojen rajoitukset on
# helppo tutkia laskemalla numeron jakojäännös sadalla, ja tarpeen tullen säännöstöä on mahdollista laajentaa.
# Suurin osa tapahtumista kirjataan joko tuloihin tai menoihin. Tämän lisäksi joistakin tapahtumista siirretään osuus
# ALViin, osinkoihin ja/tai naposteltavien hankintaan.
# Kun syötetapahtuma käsitellään, lambdafunktio tekee kustakin tilinumero-rahamäärä -parista tuplen, ja lopuksi
# palauttaa nämä tuplet listana. Nämä listat tallennetaan omaan listaansa, ja yksirivisyyden aiheuttamien haasteiden
# vuoksi tuloksen sisemmät listat pitää vielä purkaa. Tämän purkamisen hoitaa ratkaisun ehkä mielenkiintoisin vaihe,
# lambda-rekursio. (lambda itse, l: itse(itse, l)) on sinänsä yksinkertainen funktio: se saa parametreina funktion
# ("itse") ja listan, eikä oikeastaan tee muuta kuin käskee funktion kutsumaan itseään. Parametrina annettu funktio
# on ns. flatten-funktio, joka "tasoittaa" annetusta listasta kaikki sisemmät listat pois. Lopputuloksena listaan
# jää erinäinen määrä tilinumero-rahamäärä -pareja, jotka on näppärä kirjoittaa tuotos-tiedostoon.
# Ratkaisu ei tee virhetarkasteluja, sillä vaikka ne olisivat mahdollisia (ja usein tarpeellisia), halusin pitää
# koodin "järkevän" pituisena.
# Ja jälleen kerran: pahoitteluni sille raukalle, joka joutuu koodiani analysoimaan :D
(lambda syote, tuotos: print("❄") or not open(tuotos, "w", encoding="utf-8") or [open(tuotos, "a", encoding="utf-8").write("{:s},{:s}\n".format(str(tili), str(raha))) for tili, raha in (lambda itse, l: itse(itse, l))(lambda itse, l: [] if not len(l) else [l[0]] + itse(itse, l[1:]) if type(l[0]) != list else itse(itse, l[0]) + itse(itse, l[1:]), [(lambda file: [tuple(file.readline().strip().split(","))] + [(lambda tili, raha, kohde: [(tili, round(raha * 0.76 * 0.9 * 0.95, 2)), (1700, raha), (2700, round(raha - raha * 0.76, 2)), (4700, round(raha * 0.76 * 0.1, 2)), (3700, round(raha * 0.76 * 0.9 * 0.05, 2))] if kohde == 10 else [(tili, round(raha * 0.6, 2)), (1700, raha), (3700, round(raha * 0.1, 2)), (4700, round(raha * 0.3, 2))] if kohde == 20 else [(tili, raha), (1700, raha)] if kohde == 30 else [(tili, raha), (700, raha), (4700, -raha)] if kohde == 40 else [(tili, raha), (700, raha)] if kohde in [50, 60] else [(tili, raha), (700, raha), (3700, -raha)] if kohde == 70 else [(tili, raha)])(tili, raha, tili % 100) for tili, raha in [tuple(map(int, line.strip().split(","))) for line in file.readlines()]])(open(syote, "r", encoding="utf-8"))])])("syote.txt", "tuotos.txt")
@juvuorin
Copy link
Author

Kiteen tuotos on taattua tavaraa. Tuple on hyvä valinta yksinkertaiseksi tietorakenteeksi ja sitä soisi käytettävän useamminkin, esim. Javassa joudutaan käyttämään ko. tilanteissa luokkia (class), sillä tuplea ei ole tarjolla. Tuple on ohjelmoinnin harjoitteluun mitä erinomaisia rakenne tietojen "kytkemiseen".

Koska yksi vienti voi johtaa useampaan, syntyy tilanne, missä on listoja listoissa. Näin ollen käsittelyn päätteeksi pitää rakenteen syvyyttä madaltaa ja se hoituu flatten-funktiolla, kuten kommenteista saamme lukea. Automaattisen käsittelyn logiikka on helposti luettavissa suoraan koodista, vaikka if-else -rakenteet hieman "kohinaa" aiheuttavatkin. Koodin kirjoittaja ymmärtää selvästi hyvin, että "flatten" on jotain "yleistä", mikä tulee vastaan tämäntyyppisiä ongelmia näin ratkoessa. Hyvä!

Ns. domain-kohtaisesta ymmärryksestä kertoo, että laskutoimitukset pyöristetään kahden desimaalin tarkkuuteen. Pythonin tiedostonkäsittely uppoaa siististi koodiin ja se näyttelee roolia, mikä sille kuuluu - sivuvaikutukset ovat sivuosassa.

Lisäviennit syntyvät ilman turhia välivaiheita suoraan tupleina, kuten tässä [(tili, raha), (700, raha), (4700, -raha)]. Tuplet olisi helppo korvata tiedolla, joka tulisi vaikkapa jostain ulkoisesta tietolähteestä ja toisaalta koodi on niin yksinkertainen, että logiikkaa voisi säilyttää ja ylläpitää sellaisenaan, skriptinä, ilman erityistä tarvetta "logiikan" muulle säilömiselle. Taloushallinnon konsultti voisi pienen koulutuksen päätteeksi olla valmis tekemään koodiin muutoksia, vaikka yksiriviseenkin :)

Kuten esimerkistä nähdään [[]] rakenne syntyy melko yksinkertaisessakin algoritmissa ja Kide on yksirivisessä ratkaisussaan hoitanut ongelman rekursiivisella lambda -funktiolla.

Koodin luettavuus on taattua Kide-laatua, josta saimme nauttia jo Kiteen vuoden 2020 kisavastauksessa. Vastaus on kuitenkin erinomainen esimerkki siitä, miten yksinkertaisia ongelmia kannattaa ratkaista yksinkertaisesti. Flatten funktion sukulaisfunktio flatMap eli bind on ilman muuta tutustumisen arvoinen, jos tämäntyyppiset ongelmanratkaisutehtävät kiinnostavat. Mm. Haskell-kielessä [[]] > [] muunnos voidaan ajatella "monadisena kuorintana", missä säilytetään alkuperäinen rakenne, eli "flätti" lista.

List comprehension taitaa olla näissä yksirivisissä ratkaisuissa pakollinen "paha" :)

Pyrimme vuoden 2022 Koiramaisessa ohjelmointikilpailussa laatimaan tehtävä, missä ratkaisuun joutuisi lisäämään muutamia tarpeettomia lohkoja, jotta näksimme vuoden 2020 kisan vertaisia populaarikulttuuriviittauksia.

Hauska ratkaisu kaiken kaikkiaan. Kiitos vastauksesta ja mitä parhainta kevättä ja Vappua!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment