Som B.S. Ingemann skrev i 1850, så er tider noget, der kommer og henruller. Det gælder derfor om at få sig en evighedskalender, og det kan ikke gå for hurtigt. Så dagens projekt er altså som følger:
En dynamisk kalender, der strækker sig tilbage til Ruder Konges tid og kører frem til solen brænder ud. Eller sådan cirka. Den skal vise ugedag, ugenumre, forskydelige og faste helligdage og mærkedage og vise symboler for månefaser (fuldmåne, nymåne og aftagende/tiltagende halvmåne). Vi laver den responsivt så der vises 12 måneds-kolonner på større desktopskærme, 2 x 6 månedskolonner på mediumskærme og 4 x 3 månedskolonner på mobil. Det hele laves i ren javascript, css og html. Back to basic: ingen jQuery, Bootstrap, Vue el.lign frameworks.
Det færdige resultat af dagens øvelse kan ses her:
Dansk evighedskalender
eller med fuld kildekode på Codepen
Indledende om fup eller fakta
Enhver idiot kan komme og påstå, at det var fuldmåne den 25. maj 1567 eller at påskedag faldt den 29. marts 1812. Der er sjældent nogen, der står i kø for at protestere. Her er derfor nogle autoritative kilder, så vi kan kontrollere, at den dynamiske kalender, som vi laver, viser troværdige data:
R. W. Bauer: Calender for Aarene fra 601 til 2200 efter Christi Fødsel. En klassiker og kalender-bibel. Den findes i adskillige versioner på internettet og kan bruge til at kontrollere, om vi har fat i de rigtige datoer for påskedag og andre helligdage i både den julianske og gregorianske kalendertid:
Calender for Aarene fra 601 til 2200 efter Christi Fødsel
Og til kontrol af månefaser kan f.eks. anvendes:
Timeanddate.com
Astropixels.com
Hvem kontrollerer så dem, der kontrollerer? I Bauers kalender fremgår det f.eks. at Marie Bebudelsesdag altid falder den 25. marts (hvilket uheldigvis også er den viden man får, når man googler dagen). Men ved helligdagsreformen i 1770 blev Marie Bebudelsesdag i Danmark flyttet til søndagen før palmesøndag, og varierer altså fra år til år. Det lyder måske nørdet, men det har den vigtige lære, at man aldrig skal stole blindt på autoriteter. Tilsvarende kløjes timeanddate.com i fastlæggelse af månefaser før 1700, hvilket de dog også selv beskriver. Her må astropixels.com nok anses som mere troværdig.
Nå, men lad os komme i gang.
Ugedage
For at finde det rigtige ugedags-bogstav for de enkelte datoer i året kan f.eks. findes ugedagen for 1. januar i året og så blot loope gennem året og frem til 31. december. Javascripts Date-objekt er med en udsøgt logik bygget, så januar har værdien 0. Den returnerede værdi for ugedag er fra 0=søndag til 6 = lørdag. At finde ugedagen for f.eks. 1. januar 2020 vil blive
new Date(2020,0,1).getDay(); // returnerer 3 = onsdag
Før der loopes igennem året er der lige et par ting at ordne. For det første at udrede om året er skudår. Det er tilfældet, hvis årstallet kan deles med 4, dog ikke, hvis det kan deles med 100, bortset fra hvis det kan deles med 400. Det er nemmere at forstå i javascript:
let leapYear = function(year) { return ((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0); }
I så tilfælde skal antallet af dage i februar jo ændres fra 28 til 29. Endelig om det valgte år er før eller efter 1700. Den 1. marts 1700 overgik vi fra den julianske til den gregorianske kalender, hvilket indebar at den 18. februar blev efterfulgt af den 1. marts. Tiden imellem de to datoer er altså en spøgelsesperiode, der aldrig har eksisteret.
Det har javascripts Date-objektet naturligvis ikke indbygget viden om:
new Date(1700,2,1).getDay(); // returnerer 1 = mandag - Rigtigt new Date(1700,1,18).getDay(); // returnerer 4 = torsdag - Forkert, skulle være 0=søndag
Der findes måder at beregne sig frem til ugedag i den julianske kalender, f.eks:
let JulianWeekDay = function(y, m, d) { if (y < 0) ++y; if (m > 2) m = m + 1; else { --y; m = m + 13; } jul = Math.floor(365.25 * y) + Math.floor(30.6001 * m) + d + 1720995; return Math.round((jul + 1) % 7); }
Men hvorfor gøre tingene mere komplicerede end de er? Med viden om at Date-objektet for datoer før 1. marts 1700 returnerer 4 for meget, kan vi jo blot trække dem fra igen, så vi for kalenderår til og med år 1700 ikke beder om ugedagen for 1. januar, men ugedagen for 28. december året før. Der kan nu laves en funktion, der altid vil give den korrekte ugedag for første dag i et given år:
let firstDayInYear = function(year){ if (year > 1700) { return new Date(year, 0, 1).getDay(); //Find ugedag 1. januar } else { return new Date(year-1, 11, 28).getDay(); //Hvis 1700 eller ældre: træk fire dage fra for korrekt ugedag } }
Ugenr
Uge 1 i et år er i følge ISO-8601. der benyttes i Danmark, den første uge, der indeholder en torsdag. Der er mange måder i javascript til at finde ugenummeret på, men jeg har anvendt følgende og tilføjet det som extension til Date-objektet:
Date.prototype.getWeek = function() { var date = new Date(this.getTime()); date.setHours(0, 0, 0, 0); date.setDate(date.getDate() + 3 - (date.getDay() + 6) % 7); var week1 = new Date(date.getFullYear(), 0, 4); return 1 + Math.round(((date.getTime() - week1.getTime()) / 86400000 - 3 + (week1.getDay() + 6) % 7) / 7); }
Nu har vi ugedage og ugenumre på plads, og via css sørger vi for at gråtone søndage og delvis gråtone lørdage, så det ligner en rigtig Mayland-kalender. Lad os tage den et skridt videre og få nogle symboler ind for datoer med fuldmåne, nymåne med videre.
Månefaser
Jeg troede at denne del af opgaven var ligetil. Ifølge min indskrænkede astronomiske viden er månens cyklus fra fuldmåne til fuldmåne 29.5 dage og at jeg håbede derfor – med udgangspunkt i en kendt fuldmånedato – at kunne loope frem og tilbage i tiden, på samme måde som med ugedage. Jeg lærte hurtigt at der blot er tale om et gennemsnit og at perioden kan variere en del. I en påstået evighedskalender vil det give utroværdige udsving, der ikke tåler kontrol af mere autoritative kilder.
Lidt research ledte mig frem til Jean Meeus bog “Astronomical Algorithms“ fra 1991, der synes at være værket over dem alle, men allerede på ca. side 30, måtte jeg indse, at jeg ikke fattede en bønne. Amatørastronom bliver jeg aldrig. Heldigvis er der skarpere knive i skuffen, der har porteret Meeus formler til javascript: https://github.com/Fabiz/MeeusJs
Med dette herlige værktøj (<23 kB) kan månens fase på en given dag hentes med:
let moonPhase = function (date) { var jdo = new A.JulianDay(date); var coord = A.EclCoord.fromWgs84(55.719437, 13.197304, 50); //København var suntp = A.Solar.topocentricPosition(jdo, coord, true); var moontp = A.Moon.topocentricPosition(jdo, coord, true); var i = A.MoonIllum.phaseAngleEq2(moontp.eq, suntp.eq); return A.MoonIllum.illuminated(i); }
Som angivelse af månefaser i kalenderen anvendes de fire Unicode-symboler: 🌕 🌗 🌑 🌓 .
Påskedag og andre helligdage
Påske er den vigtigste højtid i kristendommen, og dermed er påskedag også den vigtigste dato at få fastlagt i kalender-sammenhæng, da en lang række andre helligdage og mærkedage beregnes ud fra påskedag. Det gælder f.eks. kyndelmisse, fastelavn, palmesøndag, skærtorsdag, langfredag, pinse m.fl.
Som det er tilfældet med den islamiske ramadan, så fastlægges den kristne påske på baggrund af fuldmåne, idet påskesøndag er den første søndag efter første fuldmåne efter 21. marts. Desværre gælder det for påske som for ramadan, at det ikke nødvendigvis er den astronomiske fuldmåne, der gælder. For de, der måtte interessere sig for datoberegning af påsken, vil jeg henvise til Claus Tønderings fantastiske Calendar FAQ og til Wikipedias magelige trin-for-trin gennemgang af beregning af dato for påskedag. I javascript vil beregningen være som følger:
//Se Wikipedia: Beregning af datoen for påskedag // https://da.wikipedia.org/wiki/P%C3%A5ske#Beregning_af_datoen_for_p%C3%A5skedag function GregorianEaster(y) { Y = parseInt(y); var C = Math.floor(Y / 100); var N = Y - 19 * Math.floor(Y / 19); var K = Math.floor((C - 17) / 25); var I = C - Math.floor(C / 4) - Math.floor((C - K) / 3) + 19 * N + 15; I = I - 30 * Math.floor((I / 30)); I = I - Math.floor(I / 28) * (1 - Math.floor(I / 28) * Math.floor(29 / (I + 1)) * Math.floor((21 - N) / 11)); var J = Y + Math.floor(Y / 4) + I + 2 - C + Math.floor(C / 4); J = J - 7 * Math.floor(J / 7); var L = I - J; var M = 3 + Math.floor((L + 40) / 44); var D = Math.floor(L + 28 - 31 * Math.floor(M / 4)); if (y == 1744) return Y + "-" + padout(3) + "-" + padout(29); return Y + "-" + padout(M) + "-" + padout(D); // 12-04-2020 }
Den gælder imidlertid kun for den gregorianske kalender, så for tiden før 1700 har jeg derfor tyet til udvalgt kode fra J. R. Stocktons “The Calculation of Easter Sunday” der er gyldig for den julianske kalender:
// JulianEaster baeret på J R Stocktons "The Calculation of Easter Sunday": // https://people.cs.nctu.edu.tw/~tsaiwn/sisc/runtime_error_200_div_by_0/www.merlyn.demon.co.uk/estr-bcp.htm function JulianEaster(Y) { Y = parseInt(Y); function GoldenNumber(Yr) { return Mod(Yr, 19) + 1 } function Mod(X, Y) { return X - Math.floor(X / Y) * Y } function GNtoJulianPFM(GN) { // By analogy with Gregorian data // Golden Number to PFM as Day-of-March, valid perpetually var T = 21 + (GN * 19 - 4) % 30; return T } function Div(X, Y) { return Math.floor(X / Y) /* full range */ } function JulianSundayNumber(Y) { var Z = 4 + Y + Div(Y, 4) return 6 - Mod(Z, 7) /* 0-6 matches Sunday Letter A-G */ } function JPFMtoDate2(Year, DM) { var SN // Ahead to Julian Sunday SN = JulianSundayNumber(Year) return YDoMtoYMonDD(Year, SundayAfterPFM(DM, SN)) } function YDoMtoYMonDD(Y, DM) { return Y + DoMtoMonDD(DM) } function DoMtoMonDD(DM) { return DM > 31 ? "-04-" + LZ(DM - 31) : "-03-" + DM } function LZ(n) { return (n != null && n < 10 && n >= 0 ? "0" : "") + n } function SundayAfterPFM(DM, SN) { // plus 1..7 days to Sunday, J & G return DM + 1 + (60 + SN - DM) % 7 /* Day-of-March */ } Y = parseInt(Y); var GN = GoldenNumber(Y) var PFM = GNtoJulianPFM(GN) return JPFMtoDate2(Y, PFM) }
Så for at finde påskedag for et givent år, hvad enten det er julianske eller gregoriansk kalendertid, vil det altså være:
function Easter(y) { if (y >= 1700) { return GregorianEaster(y); //Gregorianske kalender indført 1.marts 1700 (altså før påskedag i året) } else { return JulianEaster(y); //Find påske efter juliansk kalender indtil 1. marts 1700 } }
Nu er der ingen regel uden undtagelser, og det gælder også i dette tilfælde. I år 1744 eksperimenterede vi i Danmark med at lade beregningen af påskedag bero på den astronomiske fuldmåne. Den faldt i året på lørdag den 28. marts og påskesøndag blev derfor søndag den 29. marts. I Danmark var der altså i 1744 påske en uge tidligere end i andre nordeuropæiske lande. Det var i længden noget rod, og eksperimentet kom kun til at have effekt i dette år. Men det er nok til at vi må lave en undtagelse, der er indsat som næstsidste linje i funktionen GregorianEaster:
if (y == 1744) return Y + "-" + padout(3) + "-" + padout(29);
Med dateringen for påskedag på plads, kan vi fastlægge et stort antal andre helligdage og mærkedage. Jeg har valgt at indsætte dem i et JSON-array med formatet:
{ "danish": "Pinsedag", // Dansk tekst for dagen "latin": "Pentecoste", // Latinsk tekst for dagen - for historikere og slægtsforskere "holliday": true, // Skal dagen gråtones som helligdag? "primary": true, // Skal dagen medtages i den minimalistiske kalender "easterDiffDays": 49 // Er dagen afhængig af påskedag, da med hvor mange dage? }
Herefter er der kun at køre gennem kalenderårets dage og indsætte de nødvendige tekster og symboler.
Plads til forbedring
Den dynamiske evighedskalender viste sig at være lidt mere kompliceret, end jeg havde forudset, og da min personlige arbejdskalender desværre ikke er så responsiv som denne kalender , så måtte jeg adskillige steder springe over, hvor gærdet er lavest:
Jeg nåede ikke at få meeus.js til at fungere for den gregorianske kalender og tyede derfor til loon.js (https://github.com/ryanseys) for månefaser efter 1700. Der må helt klart være plads til forbedringer her.
At udrede helligdage på en historisk korrekt måde er et større studie, der falder uden for rammerne for denne blogpost. F.eks. kom Store Bededag først til verden med helligdagsreformen i 1770 og det giver derfor ikke mening at anføre den i kalenderår tidligere end dette. Omvendt afløste den en række andre helligdage, som ikke er medtaget her.
Endelig er det ikke mig, der er Kalendervender Tage, og sikkert er der noget i kalenderhistorikken, som jeg har overset eller misforstået.
Så der er alt i alt rigeligt at arbejde videre med. God fornøjelse.
4 kommentarer
Visse år, hvor mange eller hvilke ukendt men feks 1930 eller 1931, indeholder tilsyneladende ingen december
Hej.
Tak for opmærksomheden. Fejlen skulle være rettet nu. Ellers giv lyd igen.
Systematikken mht benævnelserne ‘1 Søndag før/efter xxxx’ synes inkonsekvent. Hvis feks 6. Jan er Hellig trekongers aften må næstfølgende søndag jo være 1. s e Hellig trekongers aften, også i de tilfælde hvor Hellig trekongers aften falder på en søndag
Så skulle inkonsekvensen være rettet konsekvent.