Spring naar hoofdtekst

Persoonlijke PDF weekkalender in PHP

Geplaatst op door ,
Laatste aanpassing op .

Inleiding

Zo rond de feestdagen zoeken mijn vrouw en ik steeds weer opnieuw naar een passende weekkalender voor het daaropvolgende jaar. De ene is te groot om op te hangen, de ander biedt niet genoeg ruimte om afspraken te noteren. Een derde gaat dan weer qua budget ons boekje te buiten.

De afgelopen dagen bedacht ik, dat ik met een paar uurtjes programmeren en wat handvaardigheid inzake papier en karton best zelf zo'n kalender zou kunnen maken. Aldus geschiedde!

Basis

De basis van elke weekkalender is een verzameling van 52 of 53 groepen van 7 dagen. Onderstaand voorbeeld maakt zo'n verzameling op basis van het opgegeven jaar. Eerst worden de nieuwjaarsdagen van het huidige en opvolgende jaar bepaald. De startdatum van de eerste week kan vóór 1 januari liggen, dus telt de code terug totdat de voorliggende maandag ($startDate->format('N') == 1) is bereikt.

Daarna begint het aanmaken van de weken zolang aan de volgende voorwaarden wordt voldaan:

  • de maandag-datum vóór 1 januari van het gevraagde jaar ligt,
  • het jaar van de getoonde week gelijk is aan het gevraagde jaar, of
  • de maandag-datum vóór 1 januari van het volgende jaar ligt

Voor elke maandag worden daarna zeven dagen (DateTime-objecten) toegevoegd, in een array gegroepeerd met jaar en weeknummer als sleutel. Let daarbij op het verschil tussen format('Y') en format('o'). De eerstgenoemde is het jaartal van de opgegeven datum, de tweede het jaartal van de ISO-8601 week waartoe de datum behoort.

Een voorbeeld: maandag 30 december 2019 heeft als 'normaal' jaartal 2019. Het is echter de eerste dag van week 1 van 2020. Zie voor meer informatie de officiële documentatie van date().

<?php
$year = 2020;
$weeks = array();

$firstJan = new \DateTime($year.'-01-01');
$nextFirstJan = new \DateTime(($year+1).'-01-01');

$startDate = clone $firstJan;
while ($startDate->format('N') > 1) {
    $startDate->sub(new \DateInterval('P1D'));
}

$loopDate = clone $startDate;
while ($loopDate <= $firstJan
    or $loopDate->format('o') == $year
    or $loopDate < $nextFirstJan
) {
    $week = $loopDate->format('o-W');
    for ($i = 0; $i < 7; $i++) {
        $weeks[$week][] = clone $loopDate;
        $loopDate->add(new \DateInterval('P1D'));
    }
}

Feestdagen

Een kalenderjaar telt vele feestdagen. De feesten die altijd op dezelfde datum vallen zijn eenvoudig aan onze kalender toe te voegen:

<?php
$holidays = array();
$holidays[($year-1).'-12-25'] = '1<sup>e</sup> Kerstdag';
$holidays[($year-1).'-12-26'] = '2<sup>e</sup> Kerstdag';
$holidays[$year.'-01-01'] = 'Nieuwjaarsdag';
$holidays[$year.'-02-14'] = 'Valentijnsdag';
$holidays[$year.'-04-27'] = 'Koningsdag';
$holidays[$year.'-05-04'] = 'Dodenherdenking';
$holidays[$year.'-05-05'] = 'Bevrijdingsdag';
$holidays[$year.'-12-25'] = '1<sup>e</sup> Kerstdag';
$holidays[$year.'-12-26'] = '2<sup>e</sup> Kerstdag';
$holidays[($year+1).'-01-01'] = 'Nieuwjaarsdag';

Andere feesten zoals Pinksteren, Hemelvaart en Carnaval zijn afgeleid van het hoogfeest van Pasen. De datum van Paaszondag is op zijn beurt weer afhankelijk van de maanstand en vele andere factoren. Met onderstaande functie bepaalt PHP de datum van Paaszondag in het opgegeven jaar:

<?php
function _getEasterDatetime(int $year) : \DateTime
{
    $base = new \DateTime("$year-03-21");
    $days = easter_days($year);
    return $base->add(new \DateInterval("P{$days}D"));
}

Om de resterende feestdagen toe te voegen, moeten we ons baseren op de datum van Pasen. Omdat de add() en sub() methoden van DateTime-objecten echter van nature het moederobject veranderen, moeten we de basisdatum klonen. Ik heb er voor gekozen om deze handeling en het optellen/aftrekken in één functie samen te vatten:

<?php
function _dtWrap(\DateTime $dt, int $days) : \DateTime
{
    $outDt = clone $dt;
    $di = new \DateInterval('P'.abs($days).'D');

    if ($days < 0) {
        $outDt->sub($di);
    } else {
        $outDt->add($di);
    }
    return $outDt;
}

Het toevoegen van de variabele kerkelijke feesten kan er dan als volgt uitzien:

<?php
$dtEaster = _getEasterDatetime($year);

$holidays[_dtWrap($dtEaster, -49)->format('Y-m-d')] = 'Carnavalszondag';
$holidays[_dtWrap($dtEaster, -46)->format('Y-m-d')] = 'Aswoensdag';
$holidays[_dtWrap($dtEaster, -2)->format('Y-m-d')] = 'Goede vrijdag';
$holidays[$dtEaster->format('Y-m-d')] = '1<sup>e</sup> Paasdag';
$holidays[_dtWrap($dtEaster, 1)->format('Y-m-d')] = '2<sup>e</sup> Paasdag';
$holidays[_dtWrap($dtEaster, 39)->format('Y-m-d')] = 'Hemelvaartsdag';
$holidays[_dtWrap($dtEaster, 49)->format('Y-m-d')] = '1<sup>e</sup> Pinksterdag';
$holidays[_dtWrap($dtEaster, 50)->format('Y-m-d')] = '2<sup>e</sup> Pinksterdag';

Vaders en moeders

In Nederland worden zowel moeders als vaders geëerd met een eigen feestdag. Deze valt altijd op respectivelijk de 2e zondag van mei en de 3e zondag van juni. Met onderstaande code worden deze twee dagen aan onze kalender toegevoegd:

<?php
$dtMothersday = new \DateTime($year.'-05-01');
while ($dtMothersday->format('N') != 7) {
    $dtMothersday->add(new \DateInterval('P1D'));
}
$dtMothersday->add(new \DateInterval('P7D'));
$holidays[$dtMothersday->format('Y-m-d')] = 'Moederdag';

$dtFathersday = new \DateTime($year.'-06-01');
while ($dtFathersday->format('N') != 7) {
    $dtFathersday->add(new \DateInterval('P1D'));
}
$dtFathersday->add(new \DateInterval('P14D'));
$holidays[$dtFathersday->format('Y-m-d')] = 'Vaderdag';

mPDF

Met behulp van de mPDF-bibliotheek kun je een HTML-document omzetten naar het universele PDF-formaat. Het biedt ondersteuning voor eigen opmaak door middel van custom style sheets (CSS), eigen lettertypes (fonts) en nog véél meer. In dit geval heb ik gekozen voor één HTML-tabel van 8 rijen per pagina en week. De eerste rij bevat het weeknummer, de maand en het jaartal. De resterende rijen zijn de zeven dagen van de week met weekdag, eventuele feestdag(en) en dagnummer.

<?php
$pdf = new \Mpdf\Mpdf();

foreach ($weeks as $days) {
    $pdf->AddPage();
    $monday = reset($days);
    $sunday = end($days);

    $mWeek = strftime('%B', $monday->format('U'));
    $mMonday = strftime('%b', $monday->format('U'));
    $mSunday = strftime('%b', $sunday->format('U'));

    $yWeek = strftime('%Y', $monday->format('U'));
    $yMonday = strftime('%Y', $monday->format('U'));
    $ySunday = strftime('%Y', $sunday->format('U'));
    $ySundayShort = strftime('%y', $sunday->format('U'));

    $html = '<table>';

    $html .= sprintf(
        '<tr><th>Week %d - %s</th>',
        $monday->format('W'),
        ($mMonday == $mSunday ? $mWeek : $mMonday.' / '.$mSunday)
    );
    $html .= sprintf(
        '<th>%s</th></tr>',
        ($ySunday == $yMonday ? $yWeek : $yMonday.'/'.$ySundayShort)
    );

    foreach ($days as $ix => $day) {
        $dayYMD = $day->format('Y-m-d');
        $isHoliday = array_key_exists($dayYMD, $holidays);

        $html .= '<tr>';
        $html .= sprintf(
            '<td>%s%s</td>',
            strftime('%A', $day->format('U')),
            ($isHoliday ? '<br />'.join('<br />', $holidays[$dayYMD]) : '')
        );
        $html .= sprintf(
            '<td>%d</td>',
            $day->format('d')
        );
        $html .= '</tr>';
    }
    $html .= '</table>';
    $pdf->WriteHTML($html);
}
$pdf->Output('Weekkalender-'.$year.'.pdf', D::DOWNLOAD);

Resultaat

De bovenstaande code is zover mogelijk vereenvoudigd om makkelijker te kunnen begrijpen wat er gebeurt. De definitieve code van dit project heb ik openbaar gemaakt op mijn pagina bij GitHub en bevat onder andere diverse opmaaktrucjes om één en ander passend te maken op A4.

Voor wie niet kan wachten, heb ik ook een voorbeeld-PDF toegevoegd over het jaar 2023 (waar vaderdag samenvalt met de verjaardag van één van de groten der aarde). Voortaan hoeven we niet meer op zoek naar een passende weekkalender; we genereren een PDF-document en drukken dat af. Een paar minuten knutselen en we kunnen er weer een jaar tegenaan!

Inhoudsopgave

Atom-feed Atom-feed van FWiePs weblog

Artikelen


Categorieën

Doorzoek de onderstaande categorieën om de lijst met artikelen te filteren.