Beispiel 4 - Kalender


... [ Seminar "Haskell" ] ... [ Inhaltsverzeichnis ] ... [ zurück ] ... [ weiter ] ...

Übersicht: Beispiel 4 - Kalender


Einleitung

Ziel dieses Beispiels soll es sein, Funktionen zur Erzeugung von Kalendern (monats- und jahresweise) zu definieren. Dazu definieren wir zuerst ein Datenmodell zur Repräsentation von Kalendern (das vollständig mit Typsynonymen auskommt), anschließend Funktionen zur Erzeugung dieser Strukturen und abschließend geben wir beliebige Exemplare dieser Datenstruktur formatiert als Strings aus:

01 -- Repräsentation eines Datums
02 type Date    = (Day,Month,Year)
03 type Day     = Int
04 type Month   = Int
05 type Year    = Int
06 
07 -- Repräsentation eines Wochentages (Sonntag == 0)
08 type Dayname = Int
4-1.txt

Codebeispiel 35

Bemerkenswert ist hierbei die Repräsentation des Tages, wir stellen sowohl den Tag innerhalb eines Monats als auch den Tag innerhalb einer Woche mit Zahlen dar. Warum auch anders? Es würde ein leichtes sein, die Zahlendarstellung des Wochentages in einen korrespondierenden String ("Montag", "Dienstag", ...) zu transformieren.


[ nach oben ]

Bestimmung von Wochentagen

Eine wesentliche Herausforderung besteht tatsächlich in der Berechnung der Wochentage, also der Frage, um welchen Wochentag es sich bei einem gegebenen Datum handelt. Wir reduzieren dieses Problem auf ein einfacheres: Welcher Wochentag ist der erste Tag einen gegebenen Monats? Hiervon ausgehend läßt sich leicht der Wochentag eines jeden Tages dieses Monats berechnen:

01 -- Bestimmt den Wochentag eines beliebigen Datums mittels des Abstandes zum Wochentag des ersten Tages des Monats
02 day :: Date -> Dayname
03 day (d,m,y) = (fstday (m,y) + d - 1) mod 7
4-2.txt

Codebeispiel 36

Es gilt also die hier genutzte fstday-Funktion auszufüllen. Wir führen diese Funktion erneut auf etwas anderes zurück und definieren sie als indizierten Zugriff auf die Liste der ersten Wochentage aller Monate des Jahres.

01 -- Bestimmt den ersten Wochentag eines Monats durch indizierten Zugriff in die Liste der ersten Wochentage eines Jahres
02 fstday :: (Month,Year) -> Dayname
03 fstday (m,y) = (fstdays y)!!(m-1)
4-3.txt

Codebeispiel 37

Die Funktion fstdays, die die sortierte Liste der ersten Wochentage aller Monate eines Jahres erzeugt, nimmt die Nummern der ersten Tage aller Monate eines Jahres und rechnet sie modulo 7, um den jeweiligen Wochentag zu erhalten. Die Nummern sind die Nummern des Tages innerhalb des jeweiligen Jahres erhöht um die Wochentagsziffer des 1. Januars (siehe mtotals).

01 -- Bestimmt die ersten Wochentage der Monate eines Jahres
02 fstdays :: Year -> [Dayname]
03 fstdays = take 12 . map (`mod` 7) . mtotals
4-4.txt

Codebeispiel 38

mtotals berechnet aus der Länge der einzelnen Monate für jeden Monat die Nummer seines ersten Tages innerhalb des gesamten Jahres. Dazu wird die Liste der Monatslängen hergenommen und mit scan für jede Teilliste davon (also für jeden Monat) die Summe der Vormonate kummuliert und um den Wochentag des 1. Januars erhöht. Diese Funktion erzeugt eine Liste der Länge 13, das letzte Element repräsentiert den ersten Tag des Folgejahres (da zum 1. Dezember natürlich noch die Länge des Dezembers addiert wird und somit zum 1. Januar führt).

01 -- Bestimmt eine Liste mit den Nummern der 1.Wochentage der Monate eines Jahres bezogen auf den Wochentag des 1. Januars
02 mtotals :: Year -> [Int]
03 mtotals y = scanl (+) (jan1 y) (mlengths y)
4-5.txt

Codebeispiel 39

mlengths beliefert mtotals mit den Monatslängen eines Jahres. Diese Liste ist bis auf den Februar konstant, der Februar erhält in Abhängigkeit von einem Schaltjahr-Prädikat 29 (Schaltjahr) bzw. 28 Tage zugeordnet.

01 -- Liefert eine Liste der Monatslängen eines Jahres, der Februar wird schaltjahresabhängig bestimmt
02 mlengths :: Year -> [Int]
03 mlengths y = [31,feb,31,30,31,30,31,31,30,31,30,31]
04              where feb = if leap y then 29 else 28
4-6.txt

Codebeispiel 40

leap ist ein Prädikat, daß für ein bestimmtes Jahr die Schaltjahreigenschaft berechnet. Zugrunde liegt die Definition "Schaltjahre sind diejenigen Jahre, deren Jahreszahl durch 4 teilbar ist mit Ausnahme derjenigen, deren Jahreszahl durch 100, nicht aber durch 400 teilbar ist.".

01 -- Prädikat zum Testen auf ein Schaltjahr
02 leap :: Year -> Bool
03 leap y = if y mod 100 = 0 then (y mod 400 = 0) else (y mod 4 = 0)
4-7.txt

Codebeispiel 41

Bleibt das Problem, den Wochentag des 1. Januars eines gegebenen Jahres zu berechnen. Der 1. Januar des Jahres 1 (im gregorianischen Kalender) war ein Montag, daher lautet unsere Lösung, alle Jahreslängen seit dem 1. Jahr aufzuaddieren, 1 hinzuzufügen und module 7 zu rechnen. Wir nehmen für jedes Jahr 365 Tage an, addieren alle Schaltjahre (x div 4 entsprechend aller durch 4 teilbarer Jahre), subtrahieren die Schaltjahre, die keine sind (x div 100 entsprechend aller durch 100 teilbarer Jahre) und addieren wieder die Schaltjahre drauf, die doch welche sind (x div 400 entsprechend aller Jahre, die durch 100 und 400 teilbar sind).

01 -- Bestimmt den Wochentag des 1. Januars eines Jahres
02 jan1 :: Year -> Dayname
03 jan1 y = (365 * x + x div 4 - x div 100 + x div 400 + 1) mod 7
04          where x = y - 1
4-8.txt

Codebeispiel 42


[ nach oben ]

Erweiterung der Bildfunktionen aus Beispiel 1

Wir kommen damit zum Problem der Darstellung von Kalendern. Wenn wir uns abstrakt vorstellen, wir könnten eine Liste von Monaten erzeugen (abstrakt heißt hier, daß nicht nur eine Liste von 1 bis 12 gemeint ist...), dann wäre ein guter erster Schritt zur Visualisierung eine Umwandlung in eine Liste von Monatsbildern (durch eine Funktion, die aus einem Monat ein Bild macht). Damit erhalten wir 12 einzelne Bilder - wir wollen aber wie in Kalendern üblich die Monate gruppieren, und zwar zu stets 3 Monaten nebeneinander (und das natürlich 4 mal). Wir benötigen also eine Funktion, die aus einer Liste von Bildern eine Liste von Bildlisten macht, von denen jede 3 Bilder beinhaltet. Aus unserer bisherigen Arbeit ( [Monat] -> [Monatsbild] ) macht dies ( [Monatsbild] -> [[Monatsbild]] ). Jetzt müssen wir uns daran machen, aus einer Menge Einzelbilder ein einziges Ergebnisbild (den Kalender) zu machen. Wir könnten z.B. die Monatsbildlisten (die 3er-Gruppen) jeweils horizontal mit sideBySide zusammensetzen bzw. mit einer Funktion, die dies nicht nur auf 2 Bildern, sondern auf einer Liste von Bildern durchführen kann. Dies entspricht ( [[Monatsbild]] -> [Monatsbild] ), also immer noch einer Liste von Bildern, von denen jedes einer 3er Gruppe unseres Kalenders entspricht. Mit einer Listenversion von above können wir diese Bilder "stapeln" und erhalten tatsächlich ein einziges Ergebnisbild ( [Monatsbild] -> Kalenderbild ). Die besagten Listenversionen von sideBySide (diese wird spread heißen) und above (diese wird stack heißen) müssen allerdings noch verbessert werden, ohne weiteres erhalten wir so einen sehr gedrängten Kalender - wir wollen Varianten von spread und stack (spreadWith und stackWith) haben, die zwischen den zusammengesetzten Bildern eine parametrisierte Anzahl an Leerzeilen bzw. -spalten erzeugen. Wir definieren nun alle oben genannten Funktionen, die man als Erweiterung unserer aus Beispiel 1 bekannten Bildfunktionen auffassen kann:

01 -- Erzeugt ein einfarbiges Bild bestimmter Dimensionen
02 blank :: Pixel a => (Height,Width) -> a -> Picture a
03 blank (h,w) = (replicate h . replicate w)
04 
05 -- Konkateniert eine Liste von Bildern übereinander
06 stack :: [Picture a] -> Picture a
07 stack = foldr1 above
08 
09 -- Konkateniert eine Liste von Bildern übereinander und fügt Leerzeilen zwischen den Bildern ein
10 stackWith :: Int -> [Picture a] -> Picture a
11 stackWith h = foldr1 (#)
12        where p # q = p `above` (blank (h,width q) `above` q)
13 
14 -- Konkateniert eine Liste von Bildern nebeneinander
15 spread :: [Picture a] -> Picture a
16 spread = foldr1 sideBySide
17 
18 -- Konkateniert eine Liste von Bildern nebeneinander und fügt Leerspalten zwischen den Bildern ein
19 spreadWith :: Width -> [Picture a] -> Picture a
20 spreadWith w = foldr1 (#)
21  where p # q = p `sideBySide` (blank (height q,w) `sideBySide` q)
22 
23 -- Teilt ein Bild in eine Liste einer definierten Anzahl von Bildern auf
24 group :: Int -> [a] -> [[a]]
25 group n xs = if null ys then [] else ys : group n zs
26              where (ys,zs) = splitAt n xs
4-9.txt

Codebeispiel 43



[ nach oben ]

Montage eines Jahreskalenders

Nun beginnen wir mit der Nutzung dieser Funktionen zur Realisierung unserer Kalenderdarstellung.

01 -- Allgemeine Funktion zur Erzeugung des Jahreskalenders
02 year :: Year -> Picture Char
03 year = stackWith 1 . map (spreadWith 4) . group 3 . map picture . months
04 
05 -- Erzeugt eine Liste von Monatsdeskriptoren (Tupel) für ein Jahr
06 months :: Year -> [(String,Year,Dayname,Int)]
07 months y = zipp4 (mnames,replicate 12 y,fstdays y,mlengths y)
08 
09 -- Konvertiert ein Tupel 4 beliebiger Listen in eine Tupelliste
10 zipp4 :: ([a],[b],[c],[d]) -> [(a,b,c,d)]
11 zipp4 (w:ws,x:xs,y:ys,z:zs) = (w,x,y,z) : zipp4 (ws,xs,ys,zs)
12 zipp4 (_,_,_,_) = [] 
13 
14 -- Liste von Monatsnamen
15 mnames :: Picture Char
16 mnames = ["Januar", "Februar", "Maerz", "April", "Mai", "Juni", "Juli", "August", "September", "Oktober", "November", "Dezember"]
17 
18 -- Erstellt gemäß einem Tupel aus Monatsname, Jahr, 1. Wochentag und Monatslänge das Bild eines Monats
19 picture :: (String,Year,Dayname,Int) -> Picture Char
20 picture (m,y,d,s) = above (cheading (m,y)) (entries (d,s))
4-10.txt

Codebeispiel 44

year ist die allgemeine Funktion und erzeugt das Kalenderbild eines Jahres. Dazu wird eine Liste mit Monatsdeskriptoren erzeugt (months), durch Anwendung der Funktion picture auf diese 12 Deskriptoren wird daraus eine neue Liste von Monatsbildern generiert, die zu 3er-Listen gruppiert wird. Durch Anwendung von spreadWith 4 wird jede 3er-Gruppe zu einem einzigen breiten Bild mit einem Abstand von 4 weißen Spalten zwischen je zwei Monaten transformiert, stackWith faßt schließlich alle Gruppenbilder übereinander zum Jahreskalenderbild zusammen und packt je 1 weiße Zeile dazwischen.

Die genannte Liste von 12 Monatsdeskriptoren liefert months, wobei jeder Deskriptor ein Tupel aus 4 Elementen ist: Monatsname als String, die Jahreszahl, der erste Wochentag des Monats und die Länge des Monats in Tagen. Dazu verwendet months die zipp4-Funktion, mit der aus einem Tupel von 4 Listen (hier à 12 Elemente) eine Liste aus 4er-Tupeln (hier 12 Stück) erzeugt wird - damit können wir zur Erzeugung vier Listen verwenden, die wir bereits haben oder leicht erzeugen können: Die Monatsnamen sind eine Liste von Strings (entspricht einem Bild), die Jahreszahl brauchen wir nur 12mal zu vervielfältigen, fstdays und mlengths sind bereits definiert.

picture erzeugt wie oben angekündigt aus einem Monatsdeskriptor ein entsprechendes Monatsbild. Dazu packt es zwei Bilder übereinander, nämlich die Einträge des Monats sowie darüber ein Monatskopf:

01 -- Erzeugt den Kopf eines bestimmten Monatsbildes aus 1. Monatsname + Jahr und 2. Tageskürzeln
02 cheading :: (String,Year) -> Picture Char
03 cheading (m,y) = above (banner (m,y)) dnames
04 
05 -- Ein Bild aus einer Zeile von Tagesnamen als Kopf der Monatsdarstellung
06 dnames :: Picture Char
07 dnames = [" Su Mo Tu We Th Fr Sa"]
08 
09 -- Erzeugt ein einzeiliges Bild aus rechtsbündigem Monatsnamen und Jahreszahl
10 banner :: (String,Year) -> Picture Char
11 banner (m,y) = [rjustify 21 (m ++ " " ++ show y)]
12 
13 -- Erzeugt die formatierten Tage eines Monats als Bild. Dazu werden die Stringliste aller Tage des Monats in Wochengruppen zerlegt und diese zu Bildzeilen konkateniert
14 entries :: (Dayname,Int) -> Picture Char
15 entries = map concat . group 7 . pix
16 
17 -- Erzeugt ein Monatsbild aus Angabe des ersten Wochentages und der Monatslänge
18 pix :: (Int,Int) -> Picture Char
19 pix (d,s) = map (rjustify 3 . pic) [1-d..42-d]
20        where pic n = if 1 <= n && n <= s then show n else ""
4-11.txt

Codebeispiel 45

cheading, dnames und banner sind leicht zu durchschauen, sie erzeugen ein Bild aus den Abkürzungen der Tagesnamen, die über den Einträgen als Spaltentitel stehen werden, und darüber den Monatsnamen und der Jahreszahl.

entries soll die viereckige Anordnung der Tageszahlen in einem Bild realisieren. Dazu nimmt es von der pix-Funktion eine Liste von Strings, von denen stets 7 (korrespondierend mit den Wochentagen) nebeneinander angeordnet werden (hier wieder der Einsatz der group-Funktion). Diese Listen von Listen werden zu Listen von Strings konkateniert, entsprechend der Definition eines Bildes. Die pix-Funktion hat zur Lieferung ihrer String-Liste ein Problem zu bewältigen: Die Einträge des Monats fangen grafisch immer am Sonntag an, der 1. eines Monats ist jedoch nichts zwangsläufig auch ein Sonntag. Daher erzeugt pix zuerst eine Liste von Zahlen [1-d..42-d], die entsprechend des Abstandes zwischen Sonntag und dem tatsächlichen 1. Wochentag d des Monats mit Zahlen < 1 anfängt (für einen Sonntag beginnt sie mit 1, für einen Samstag mit 0, für einem Montag mit -1 usw.). Diese Zahlen werden anschließend jeweils in Strings umgewandelt (pic), wobei die Zahlen < 1 als Leerstrings (unsichtbar also), die gewünschten Zahlen in die korrespondierende Stringdarstellung und die Zahlen > der Monatslänge wieder als Leerstrings übersetzt werden (jeweils mit rjustify ausgerichtet auf 3 Zeichen, auch die Leerstrings).

... [ Seminar "Haskell" ] ... [ Inhaltsverzeichnis ] ... [ zurück ] ... [ weiter ] ... [ nach oben ] ...

valid html4 logo Code generated with AusarbeitungGenerator Version 1.1, weblink