Spring naar hoofdtekst

Kerkelijk jaar in PHP

Geplaatst op door ,
Laatste aanpassing op .

Inleiding

Dit is deel 1 van een tweedelig artikel over een online feestdagkalender met archief van bijbehorende bijbellezingen. Lees hier het tweede deel.

Een van mijn langstlopende programmeerprojecten is de website van het Parochiecluster U.o.W.. Het is niet alleen een homepage, maar ook een online archief van parochieblaadjes, misroosters en lezingen uit het verleden. Met name de laatstgenoemden zijn een eigen artikeltje in dit weblog waard.

Kerkelijk jaar

Een kalenderjaar begint op 1 januari en eindigt op 31 december. Het kerkelijk jaar begint bij de zondag van de 1e Advent en eindigt op de zondag van Christus Koning. Deze twee zondagen zijn afgeleid van de dag van Kerstmis (25 december) en Paaszondag, waarbij laatstgenoemde weer afhangt van de maanstand en het begin van de lente. Een kerkelijk jaar is als volgt opgebouwd:

  • Advent
  • Kerst
  • Tijd door het jaar
  • Veertigdagentijd
  • Pasen
  • Tijd door het jaar

Hier in hebben een aantal feesten een vaste datum (Kerstmis, Nieuwjaar, Allerheiligen, Allerzielen). Anderen zijn al dan niet afgeleid van deze data en vallen altijd op een zondag (Adventzondagen, Pasen, Pinksteren).

Pasen als basis

De datum van Pasen wordt in het kerkelijk jaar als basis gebruikt voor de andere feestdagen en zondagen. PHP heeft twee functies aan boord om de paasdatum voor een opgegeven jaar te berekenen. easter_date() werkt enkel binnen het bereik van de Unix-timestamp (1970-2038). easter_days() beslaat daarentegen een groter gebied (1538-4099). Om met DateTimes te kunnen werken in plaats van Unix-timestamps, heb ik de volgende functie geschreven:

<?php
private static function _getEasterSunday($year)
{
    if (!is_int($year) || $year < 1583 || $year > 4099) {
        throw new \InvalidArgumentException();
    }

    $dtEaster = new \DateTime($year.'-03-21');
    $dI = new \DateInterval('P'.easter_days($year, CAL_EASTER_ROMAN).'D');
    $dtEaster->add($dI);
    return $dtEaster;
}

Ook de datum van de 1e Adventszondag wordt als referentie gebruikt. Deze wordt op basis van de dag van Kerstmis bepaald.

<?php
private static function _getFirstAdvent(\DateTime $christmas)
{
    $dayChristmas = (int)$christmas->format('w');
    if ($dayChristmas == 0) {
        $dayChristmas = 28; 
    } else {
        $dayChristmas = 21 + $dayChristmas; 
    }
    $diAdvent1 = new \DateInterval('P'.$dayChristmas.'D');
    return self::wrap($christmas)->sub($diAdvent1);
}

Hulpfuncties

De methodes sub() en add() van DateTime-objecten voeren een bewerking uit op het gegeven object. Hiermee veranderen ze de waarde van het object zelf. Dit is voordeel en nadeel tegelijk; je kunt bewerkingen direct achter elkaar noteren:

$dt = new \DateTime();
// $dt heeft de waarde van NU

$dt->add(new \DateInterval('P1D'))->sub(new \DateInterval('P2D'));
// $dt heeft de waarde van GISTEREN

Als je, zoals in dit script, telkens weer van één datum moet afleiden, is dit directe bewerken niet gewenst. Het volgende zou immers kunnen gebeuren:

$dtPasen = new \DateTime('2017-04-16');

$dtGoedeVrijdag = $dtPasen->sub(new \DateInterval('P2D'));
// $dtPasen heeft nu dezelfde datum als Goede Vrijdag!

$dtTweedePaasdag = $dtPasen->add(new \DateInterval('P1D'));
// $tweedePaasdag valt op deze manier op zaterdag!

Om dit gedrag te omzeilen heb ik onderstaande functie geschreven; een wrapper die voor elk DateTime-object een nieuw object teruggeeft met dezelfde waarde:

<?php
/**
 * Creates a new DateTime instance from the given instance.
 *
 * @param DateTime $datetime the DateTime to wrap
 *
 * @return DateTime
 */
public static function wrap(\DateTime $datetime)
{
    return new \DateTime($datetime->format(\DateTime::ISO8601));
}

Tot slot nog een klein hulpje, om gemakkelijk op basis van een getal, een DateInterval-object aan te maken, dat met DateTime::sub() en DateTime::add() kan worden gebruikt:

<?php
/**
 * Creates a DateInterval object of the given amount of days
 *
 * @param int $dagen the amount of days
 *
 * @return \DateInterval
 */
private static function _di($dagen)
{
    if (!is_int($dagen) || $dagen < 0) {
        return new \DateInterval('P0D');
    } else {
        return new \DateInterval('P'.$dagen.'D');
    }
}

Soms moet je uit meerdere DateTimes kiezen welke het dichtst bij een bepaalde referentiedatum ligt. Dat kan met onderstaande functie:

<?php
/**
 * Finds the chronologically closest DateTime
 *
 * @param DateTime[] $a             the array to search
 * @param DateTime   $refdate       the reference date
 * @param bool       $includePast   include dates in the past
 * @param bool       $includeFuture include dates in the future
 * 
 * @return DateTime
 */
private static function _getClosest(
    $a, $refdate, $includePast = true, $includeFuture = true
) {
    $smallestDiff = 0;
    $smallestKey = null;

    foreach ($a as $k => $v) {
        $diff = $refdate->diff($v, false);
        $diff = (int)$diff->format('%r%a');

        if ((($includePast && $diff <= 0) || ($includeFuture && $diff >= 0))
            && (($includePast && (is_null($smallestKey)
            || $diff > $smallestDiff)) 
            || ($includeFuture && (is_null($smallestKey)
            || $diff < $smallestDiff)))
        ) {
            $smallestDiff = $diff;
            $smallestKey = $k;
        }
    }
    return is_null($smallestKey) ? false : $a[$smallestKey];
}

Welke feestdag?

Dan volgt nu de kern van het artikel: de bepaling op welke feestdag een opgegeven datum valt. Dit proces bestaat uit de volgende stappen:

  • data van Pasen, Kerstmis en 1e Advent bepalen
    (voor zowel het vorige, huidige als volgende jaar)
  • referentiedatum bepalen
  • afgeleide feestdagen bepalen
  • controleren of de gezochte datum één van de gevonden data is
    (zo ja, hebben we ons antwoord!)
  • feestdagen bepalen die voorrang hebben op de reguliere zondag
  • controleren of de gezochte datum één van de gevonden data is
    (zo ja, hebben we ons antwoord!)
  • lopen door de reguliere zondagen, van 2 tot Aswoensdag, van 33 tot H.Hart
  • controleren of de gezochte datum één van de gevonden data is
    (zo ja, hebben we ons antwoord!)
  • feestdagen bepalen die geen voorrang hebben op de reguliere zondag
  • controleren of de gezochte datum één van de gevonden data is
    (zo ja, hebben we ons antwoord!)
  • de feestdag is onbekend
<?php
private function _determineHoliday()
{
    $y = (int)$this->_dt->format('Y');
    $yPrev = $y - 1;
    $yNext = $y + 1;
    $diJaar = new \DateInterval('P1Y');

    /* Datum van Pasen bepalen */
    $pasenPrev = self::_getEasterSunday($yPrev);
    $pasenNow = self::_getEasterSunday($y);
    $pasenNext = self::_getEasterSunday($yNext);

    /* Datum van Kerst bepalen */
    $kerstPrev = new \DateTime($yPrev.'-12-25');
    $kerstNow = new \DateTime($y.'-12-25');
    $kerstNext = new \DateTime($yNext.'-12-25');

    /* Datum van de 1e Advent bepalen */
    $adventPrev = self::_getFirstAdvent($kerstPrev);
    $adventNow = self::_getFirstAdvent($kerstNow);
    $adventNext = self::_getFirstAdvent($kerstNext);

    /* Juiste referentie-data bepalen */
    $eersteAdvent = self::_getClosest(
        array($adventPrev, $adventNow, $adventNext),
        $this->_dt,
        true,
        false
    );
    $kerst = new \DateTime($eersteAdvent->format('Y').'-12-25');
    $pasen = self::_getClosest(
        array($pasenPrev, $pasenNow, $pasenNext),
        $eersteAdvent,
        false,
        true
    );
    $volgendJaarAdvent = self::_getFirstAdvent(
        self::wrap($kerst)->add($diJaar)
    );

    $yAdvent = (int)$eersteAdvent->format('Y');
    $yPasen = (int)$pasen->format('Y');

    /* Alle bekende feestdagen berekenen op basis van Pasen en Kerstmis */
    $tweedeAdvent = self::wrap($eersteAdvent)->add(self::_di(7));
    $derdeAdvent = self::wrap($tweedeAdvent)->add(self::_di(7));
    $vierdeAdvent = self::wrap($derdeAdvent)->add(self::_di(7));
    $vooravondKerst = self::wrap($kerst)->sub(self::_di(1));
    $tweedeKerst = self::wrap($kerst)->add(self::_di(1));

    if ($kerst->format('w') == 0) {
        $heiligefamilie = new \DateTime($yAdvent.'-12-30');
    } else {
        $heiligefamilie = self::wrap($kerst)
            ->add(self::_di(7-(int)$kerst->format('w')));
    }

    $nieuwjaar = self::wrap($kerst)->add(self::_di(7));

    $dagNieuwjaar = (7 - (int)$nieuwjaar->format('w'));
    $diOpenbaring = new \DateInterval('P'.$dagNieuwjaar.'D');
    $openbaring = self::wrap($nieuwjaar)->add($diOpenbaring);

    $doopRefDate = new \DateTime($yPasen.'-01-07');
    while ($doopRefDate->format('w') != 0) {
        $doopRefDate->add(self::_di(1));
    }
    if ($doopRefDate == $openbaring) {
        $doopvanheer = self::wrap($openbaring)->add(self::_di(1));
    } else {
        $doopvanheer = $doopRefDate;
    }

    $aswoensdag = self::wrap($pasen)->sub(self::_di(46));
    $vasten1 = self::wrap($pasen)->sub(self::_di(42));
    $vasten2 = self::wrap($pasen)->sub(self::_di(35));
    $vasten3 = self::wrap($pasen)->sub(self::_di(28));
    $vasten4 = self::wrap($pasen)->sub(self::_di(21));
    $vasten5 = self::wrap($pasen)->sub(self::_di(14));

    $palmzondag = self::wrap($pasen)->sub(self::_di(7));
    $wittedonderdag = self::wrap($pasen)->sub(self::_di(3));
    $goedevrijdag = self::wrap($pasen)->sub(self::_di(2));
    $tweedepaas = self::wrap($pasen)->add(self::_di(1));
    $paaszondag2 = self::wrap($pasen)->add(self::_di(7));
    $paaszondag3 = self::wrap($pasen)->add(self::_di(14));
    $paaszondag4 = self::wrap($pasen)->add(self::_di(21));
    $paaszondag5 = self::wrap($pasen)->add(self::_di(28));
    $paaszondag6 = self::wrap($pasen)->add(self::_di(35));
    $hemelvaart = self::wrap($pasen)->add(self::_di(39));
    $paaszondag7 = self::wrap($pasen)->add(self::_di(42));
    $pinksteren = self::wrap($pasen)->add(self::_di(49));
    $tweedePinkst = self::wrap($pasen)->add(self::_di(50));
    $drieeenheid = self::wrap($pasen)->add(self::_di(56));
    $sacramentsdag = self::wrap($pasen)->add(self::_di(63));
    $hhart = self::wrap($pasen)->add(self::_di(68));
    $christuskoning = self::wrap($volgendJaarAdvent)->sub(self::_di(7));

    $arr = array(
        self::DAG_ADVENT1            => $eersteAdvent,
        self::DAG_ADVENT2            => $tweedeAdvent,
        self::DAG_ADVENT3            => $derdeAdvent,
        self::DAG_ADVENT4            => $vierdeAdvent,
        self::DAG_VOORAVOND_KERST    => $vooravondKerst,
        self::DAG_KERSTMIS           => $kerst,
        self::DAG_HEILIGE_FAMILIE    => $heiligefamilie,
        self::DAG_TWEEDEKERSTDAG     => $tweedeKerst,
        self::DAG_NIEUWJAAR          => $nieuwjaar,
        self::DAG_OPENBARING         => $openbaring,
        self::DAG_DOOPVANDEHEER      => $doopvanheer,
        self::DAG_ASWOENSDAG         => $aswoensdag,
        self::DAG_VASTEN1            => $vasten1,
        self::DAG_VASTEN2            => $vasten2,
        self::DAG_VASTEN3            => $vasten3,
        self::DAG_VASTEN4            => $vasten4,
        self::DAG_VASTEN5            => $vasten5,
        self::DAG_PALMZONDAG         => $palmzondag,
        self::DAG_WITTEDONDERDAG     => $wittedonderdag,
        self::DAG_GOEDEVRIJDAG       => $goedevrijdag,
        self::DAG_PASEN              => $pasen,
        self::DAG_TWEEDEPAASDAG      => $tweedepaas,
        self::DAG_PASEN2             => $paaszondag2,
        self::DAG_PASEN3             => $paaszondag3,
        self::DAG_PASEN4             => $paaszondag4,
        self::DAG_PASEN5             => $paaszondag5,
        self::DAG_PASEN6             => $paaszondag6,
        self::DAG_HEMELVAART         => $hemelvaart,
        self::DAG_PASEN7             => $paaszondag7,
        self::DAG_PINKSTEREN         => $pinksteren,
        self::DAG_TWEEDEPINKSTERDAG  => $tweedePinkst,
        self::DAG_HDRIEEENHEID       => $drieeenheid,
        self::DAG_SACRAMENTSDAG      => $sacramentsdag,
        self::DAG_HHART              => $hhart,
        self::DAG_CHRISTUS_KONING    => $christuskoning
    );

    foreach ($arr as $k => $v) {
        if ($this->_dt == $v) {
            $this->welkeDag = $k;
            return true;
        }
    }

    /* Feestdagen die voorrang hebben op de zondagen */
    $opdrachtheer = new \DateTime($yPasen.'-02-02');
    $geboortejan = new \DateTime($yPasen.'-06-24');
    $petruspaulus = new \DateTime($yPasen.'-06-29');
    $gedaanteverandering = new \DateTime($yPasen.'-08-06');
    $mariatenhemel = new \DateTime($yPasen.'-08-15');
    $kruisverheffing = new \DateTime($yPasen.'-09-14');
    $allerheiligen = new \DateTime($yPasen.'-11-01');
    $allerzielen = new \DateTime($yPasen.'-11-02');
    $willibrord = new \DateTime($yPasen.'-11-07');
    $stjanlateranen = new \DateTime($yPasen.'-11-09');

    $arr = array(
        self::DAG_OPDRACHTHEER        => $opdrachtheer,
        self::DAG_GEBOORTEJAN         => $geboortejan,
        self::DAG_PETRUSPAULUS        => $petruspaulus,
        self::DAG_GEDAANTEVERANDERING => $gedaanteverandering,
        self::DAG_MARIATENHEMEL       => $mariatenhemel,
        self::DAG_KRUISVERHEFFING     => $kruisverheffing,
        self::DAG_ALLERHEILIGEN       => $allerheiligen,
        self::DAG_ALLERZIELEN         => $allerzielen,
        self::DAG_HWILLIBRORD         => $willibrord,
        self::DAG_STJANLATERANEN      => $stjanlateranen
    );

    foreach ($arr as $k => $v) {
        if ($this->_dt == $v) {
            $this->welkeDag = $k;
            return true;
        }    
    }

    /* Zondagen door het jaar */
    for ($i = 2; $i <= 33; $i++) {
        $loopZondag = self::wrap($doopvanheer)->add(self::_di(($i-1)*7));
        $loopZondag->sub(self::_di((int)$doopvanheer->format('w')));

        if ($loopZondag > $aswoensdag) {
            break;
        }

        $zondagConst = constant(sprintf('self::DAG_ZONDAG_DHJ_%02d', $i));
        $this->_s($zondagConst, $loopZondag);

        if ($this->_dt == $loopZondag) {
            $this->welkeDag = $zondagConst;
            return true;
        }
    }

    for ($i = 33; $i >= 2; $i--) {
        $loopZondag = self::wrap($christuskoning)
            ->sub(self::_di((34-$i)*7));

        if ($loopZondag < $hhart) {
            break;
        }

        $zondagConst = constant(sprintf('self::DAG_ZONDAG_DHJ_%02d', $i));
        $this->_s($zondagConst, $loopZondag);

        if ($this->_dt == $loopZondag) {
            $this->welkeDag = $zondagConst;
            return true; 
        }
    }

    /* Feestdagen die GEEN voorrang hebben op de zondagen */
    $hjozef = new \DateTime($yPasen.'-03-19');

    $aankondiging = new \DateTime($yPasen.'-03-25');
    if ($aankondiging >= $palmzondag && $aankondiging <= $goedevrijdag) {
        $aankondiging = new \DateTime($yPasen.'-04-08');
    }

    $onbevlekt = new \DateTime($yPasen.'-12-08');

    $arr = array(
        self::DAG_HJOZEF           => $hjozef,
        self::DAG_AANKONDIGING     => $aankondiging,
        self::DAG_ONBEVLEKTONTVANG => $onbevlekt
    );

    foreach ($arr as $k => $v) {
        if ($this->_dt == $v) {
            $this->welkeDag = $k;
            return true;
        }
    }

    /* Weekdag of onbekend */
    $this->welkeDag = self::DAG_ONBEKEND;
    return false;
} 

Wordt vervolgd

Dit is deel 1 van een tweedelig artikel over een online feestdagkalender met archief van bijbehorende bijbellezingen. Lees hier het tweede deel.

Inhoudsopgave

Atom-feed Atom-feed van FWiePs weblog

Artikelen


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