Kalendermatematik
Man skulle kunna tro att det här med veckonummer är busenkelt. Det är väl bara att börja på vecka 1 och fortsätta till vecka 52 för att sedan börja om igen? Tyvärr är det inte så enkelt. Det visar sig tvärtom vara förvånansvärt komplicerat!
TL;DR
- Oftast har ett år 52 veckor, men vissa år har 53 veckor.
- Om den 53:e veckan ska finnas eller inte beror både på vilken veckodag den 1 januari nästa år är och om nuvarande år är ett skottår eller inte.
- Skottår förekommer i genomsnitt lite mindre än var fjärde år, vilket gör modellen för när 53 veckor ska förekomma klurig.
Testa den färdiga veckonumrerings- och skottårsmodellen här:
Hur funkar det egentligen med veckonummer?
De flesta år har 52 veckor. Om man kollar i kalendern verkar dock ungefär var femte eller var sjätte år ha 53 veckor. Vad som bestämmer om den 53:e veckan ska finnas är inte så simpelt, utan något så märkligt som vilken veckodag det nya året börjar på. Om den 1 januari är en måndag, tisdag, onsdag eller torsdag blir veckan som innehåller 1 januari vecka 1. Om 1 januari är en fredag, däremot, blir den veckan 53 (trots att det är januari). Om 1 januari är en lördag blir den veckan 53 om det förra året var ett skottår och 52 om det förra året var ett normalår. Om 1 januari är en söndag blir den veckan 52 oavsett om det förra året var ett skottår eller inte.
Veckonumreringsmodell
Modellen kan tydliggöras och sammanfattas i en lista så här:
- Om 1 januari är en måndag, tisdag, onsdag, eller torsdag tillhör dagen vecka 1.
- Om 1 januari är en fredag tillhör dagen vecka 53.
- Om 1 januari är en lördag tillhör dagen vecka 53 (vid skottår) eller 52 (vid normalår).
- Om 1 januari är en söndag tillhör dagen vecka 52.
Veckodagsförskjutning utan skottår
Att ett år har 365 dagar på ett normalår påverkar veckodagsförskjutningen på så sätt att om 1 januari är en måndag ett normalår är 1 januari året efter en tisdag om det också är ett normalår. Matematiken bakom denna förskjutning är att 365 inte är jämnt delbart med 7 (antalet dagar i en vecka). Om ett år hade haft 364 dagar, däremot, hade det inte blivit någon veckodagsförskjutning mellan två normalår eftersom 364 är jämnt delbart med 7 (364/7 = 52).
Om skottår inte hade funnits hade rimligtvis var sjunde år haft 53 veckor, eftersom 1 januari då skulle vara en fredag var sjunde år (enligt den nämnda veckodagsförskjutningen), och endast dessa år skulle då ha 53 veckor enligt veckonumreringsmodellen.
Veckodagsförskjutning med skottår
Närvaron av skottår stökar till veckodagsförskjutningen ordentligt. Vid ett skottår blir veckodagsförskjutningen två dagar i stället för en. Om 1 januari ett skottår är en måndag är 1 januari året efter en onsdag.
Låt säga att år 1 är det första normalåret efter ett skottår och att 1 januari det året är en måndag. Då blir veckodagen den 1 januari åren efter enligt följande lista:
1: Måndag (normalår)
2: Tisdag (normalår)
3: Onsdag (normalår)
4: Torsdag (skottår)
5: Lördag (normalår)
6: Söndag (normalår)
7: Måndag (normalår)
8: Tisdag (skottår)
9: Torsdag (normalår)
10: Fredag (normalår)
11: Lördag (normalår)
12: Söndag (skottår)
13: Tisdag (normalår)
14: Onsdag (normalår)
15: Torsdag (normalår)
16: Fredag (skottår)
17: Söndag (normalår)
18: Måndag (normalår)
19: Tisdag (normalår)
20: Onsdag (skottår)
21: Fredag (normalår)
22: Lördag (normalår)
23: Söndag (normalår)
24: Måndag (skottår)
25: Onsdag (normalår)
26: Torsdag (normalår)
27: Fredag (normalår)
28: Lördag (skottår)
29: Måndag (normalår)
År 29 blir alltså återigen 1 januari en måndag på ett år som är första normalåret efter ett skottår. Därefter upprepas samma sekvens igen. År 1–28 är alltså en sekvens som vad gäller veckodagar ser likadan ut kommande 28 år (år 29–56). Det är egentligen inte så konstigt att sekvensen blir just 28 år. 28 är nämligen den minsta gemensamma multipeln mellan 4 (frekvensen för skottår) och 7 (antalet dagar i en vecka).
Multiplar av 4: 4, 8, 12, 16, 20, 24, 28
Multiplar av 7: 7, 14, 21, 28
Multiplarna visualiseras i listan genom att 1 januari på skottåren (som förekommer var fjärde år) behöver anta alla sju veckodagar innan samma veckodag återkommer.
Här är samma lista med bara skottåren, med tillägg för år 32:
4: Fredag (skottår)
8: Onsdag (skottår)
12: Måndag (skottår)
16: Lördag (skottår)
20: Torsdag (skottår)
24: Tisdag (skottår)
28: Söndag (skottår)
32: Fredag (skottår)
Här kan man se att alla sju veckodagar förekommer innan samma veckodag förekommer igen.
Påminn dig själv om veckonumreringsmodellen:
1 januari tillhör vecka 53 om det är en fredag både vid normalår eller skottår, eller en lördag vid skottår.
Nu kan man kolla i veckodagsförskjutningslistan och räkna hur många år i en veckodagssekvens på 28 år som kommer att få 53 veckor. Svaret är att dessa år kommer att få 53 veckor:
4: Fredag (skottår)
10: Fredag (normalår)
16: Lördag (skottår)
21: Fredag (normalår)
27: Fredag (normalår)
Under år 1–28, som utgör en sekvens, förekommer alltså 5 år med 53 veckor, resten har 52. Fyra av dessa år får 53 veckor för att 1 januari är en fredag och ett av åren får 53 veckor för att 1 januari är en lördag och att året är ett skottår. Om inte skottår hade funnits hade 4 år per sekvens haft 53 veckor. Detta stämmer med mitt tidigare uttalande om att var sjunde år borde ha 53 veckor om inte skottår hade funnits, eftersom 28/4 = 7.
Medräknat skottår förekommer som sagt 5 år med 53 veckor på en sekvens. 28/5 = 5,6, vilket betyder att 53 veckor förkommer var 5,6:e år. Detta betyder att det ibland är 5 år mellan år med 53 veckor och ibland är det 6 år mellan.
...men hur funkar det egentligen med skottår?
Att år med 53 veckor förekommer var 5,6:e år var ju lite klurigare än vad man kanske hade kunnat tro. Men det är dessvärre mer komplext än så. Jag har nämligen bortsett från den verkliga frekvensen för skottår.
Anledningen att skottår överhuvudtaget existerar är för att årstider inte ska förskjutas framåt eller bakåt i kalendern med tiden. Om varje år skulle ha 365 dagar skulle årstider, dagjämningar och solstånd förskjutas flera veckor under en period av 100 år. Ett verkligt årstidsår (även kallat tropiskt år eller naturligt år) är egentligen 365 dygn, 5 timmar, 48 minuter och 45 sekunder, vilket är lika med 365,2421875 dygn. Om ett årstidsår hade varit 365 + 1/4 = 365,25 dygn = 365 dygn och 6 timmar hade skottår var fjärde år fullkomligt kompenserat för förskjutningen. Men i verkligheten är ett årstidsår 11 minuter och 15 sekunder kortare än så, vilket betyder att en skottårsfrekvens på var fjärde år är lite för hög.
Den lite för höga skottårsfrekvensen kompenseras genom att sekelår inte är skottår. I vanliga fall är alla år som är delbara med 4 skottår (alltså exempelvis år 4, 16 eller 2020), men att sekelår inte är skottår betyder att exempelvis år 100, 1900 eller 2100 inte är skottår även om de är delbara med 4 (vilket de alltid är rent matematiskt).
Tilläggsregeln med sekelår skulle vara perfekt om ett årstidsår hade varit 365 + 1/4 - 1/100 = 365,24 dygn = 365 dygn, 5 timmar, 45 minuter och 36 sekunder. Men ett årstidsår är 3 minuter och 9 sekunder längre än så, vilket betyder att denna skottårsfrekvens skulle bli lite för låg.
Den lite för låga skottårsfrekvensen i det här fallet kompenseras genom att sekelår som är jämnt delbara med 400 utgör ett undantag från regeln om sekelår och alltså är skottår ändå. År 1900 och 2100 är inte delbara med 400 och är därför inte skottår medan år 2000 och 2400 är delbara med 400 och är skottår.
Tilläggsregeln med sekelår som är delbara med 400 skulle vara perfekt om ett årstidsår hade varit 365 + 1/4 - 1/100 + 1/400 = 365,2425 = 365 dygn, 5 timmar, 49 minuter och 12 sekunder. Men ett årstidsår är 27 sekunder kortare än så. Nu kanske du tror att skillnaden skulle föranleda en ytterligare tilläggsregel, och därefter kanske ytterligare några flera tilläggsregler, men så är det inte. Med ett kalenderår som är 27 sekunder längre än årstidsåret blir förskjutningen av årstider och liknande 7,5 timmar på 1 000 år, vilket uppenbarligen inte var ett tillräckligt stort problem.
Skottårsreglerna kan sammanfattas i listform så här:
- År som är jämnt delbara med 4 är skottår (med undantag nedan).
- År som är jämnt delbara med 100 är inte skottår trots att de är jämnt delbara med 4 (med undantag nedan).
- År som är jämnt delbara med 400 är skottår trots att de är jämnt delbara med 100.
Reglerna kan också sammanfattas i en mening så här:
De år som är jämnt delbara med 4, förutom de sekelår som inte är jämnt delbara med 400, är skottår.
Modellen som JavaScript-funktion
Med all denna kunskap borde vi kunna bygga en funktion där man skickar in ett år och får ut hur många veckor det året har. Typ så här:
function getWeeksInYear(year) {
// logik
return weeks
}
För att kunna bestämma hur många veckor ett år har behöver vi dels veta vilken veckodag 1 januari året efter är och dels om aktuellt år är ett skottår eller inte. Perfekta uppgifter för två separata funktioner!
function getWeeksInYear(year) {
// logik
return weeks
}
function getFirstWeekdayInYear(year) {
// logik
return weekday
}
function isLeapYear(year) {
// logik
return boolean
}
Vi kan börja underifrån med isLeapYear. Mitt första försök att implementera skottårsreglerna rakt av resulterade i någonting i den här stilen:
function isLeapYear(year) {
if (year % 4 === 0) {
if (year % 100 === 0) {
if (year % 400 === 0) return true
else if (year % 400 !== 0) return false
} else if (year % 100 !== 0) return true
} else return false
}
Även om funktionen gör vad den ska är den oerhört svårläst. Dels används modulus på fem ställen och gör koden plottrig, dels är det svårt att läsa if-satser som befinner sig i if-satser i flera led. Med angreppssättet att börja med den snävaste regeln kan man få till en betydligt snyggare funktion. Vi passar också på att bryta ut modulus-beräkningarna i en egen funktion:
function isLeapYear(year) {
if (isDivisibleBy(year, 400)) return true
if (isDivisibleBy(year, 100)) return false
if (isDivisibleBy(year, 4)) return true
return false
}
function isDivisibleBy(numerator, denominator) {
return numerator % denominator === 0
}
Här utnyttjar vi att år som är delbara med 400 alltid är skottår, att övriga år som är delbara med 100 inte är skottår och slutligen att övriga år som är delbara med 4 är skottår (och att resten inte är skottår). Tydligt och bra! Fördelen med att bryta ut funktionen isDivisibleBy är att den nu kan användas till övriga ställen där man kan ha nytta av att veta om ett tal är delbart med ett annat. I nästa funktion vill vi veta vilken första veckodagen nästa år är. En första tanke kanske säger att funktionen helt enkelt ska returnera första veckodagen nästa år. Men för att göra funktionen mer generellt användbar tycker jag att det är en bättre idé att funktionen returnerar första veckodagen för året som skickas in, så får man skicka in nästa år. Så här:
function getFirstWeekdayInYear(year) {
const firstDateInYear = new Date(year, 0, 1) // 0 = januari, 1 = första dagen i månaden
const firstWeekdayInYear = firstDateInYear.getDay()
return firstWeekdayInYear
}
Här används det inbyggda objektet Date, där year, månad 0 (januari) och datum 1 (första dagen i månaden) används för att få ett Date-objekt med första dagen på året som skickas in. Veckodagen beräknas med den inbyggda funktionen getDay som returnerar 0 för söndag, 1 för måndag och så vidare upp till 6 för lördag. När vi använder den här funktionen kommer vi alltså inte få tillbaka exempelvis "Tuesday", utan 2 som står för tisdag.
Hur ska vi nu göra med funktionen getWeeksInYear för att den ska returnera antalet veckor för valfritt år?
function getWeeksInYear(year) {
if (getFirstWeekdayInYear(year + 1) === 5) return 53
if (getFirstWeekdayInYear(year + 1) === 6 && isLeapYear(year)) return 53
return 52
}
Funktionaliteten i sin helhet ser alltså ut så här:
function getWeeksInYear(year) {
if (getFirstWeekdayInYear(year + 1) === 5) return 53
if (getFirstWeekdayInYear(year + 1) === 6 && isLeapYear(year)) return 53
return 52
}
function getFirstWeekdayInYear(year) {
const firstDateInYear = new Date(year, 0, 1) // 0 = januari, 1 = första dagen i månaden
const firstWeekdayInYear = firstDateInYear.getDay()
return firstWeekdayInYear
}
function isLeapYear(year) {
if (isDivisibleBy(year, 400)) return true
if (isDivisibleBy(year, 100)) return false
if (isDivisibleBy(year, 4)) return true
return false
}
function isDivisibleBy(numerator, denominator) {
return numerator % denominator === 0
}
Notera att alla negativa och positiva heltal ger ett resultat. År -3008 ger exempelvis resultatet "År -3008 har 52 veckor och är ett skottår". Naturligtvis har inte veckonumrerings- och skottårsmodellen alltid sett ut som den gör i dag, och den ser inte heller likadan ut i andra länder, så resultatet representerar endast den nuvarande modellen i Sverige.
Bättre alternativ än att göra veckoberäkningar själv
Jag tror att det är nyttigt att öva på att formulera korrekta och lättlästa funktioner när man programmerar. I fallet med beräkning av veckonummer skulle jag dock sällan rekommendera att göra uträkningar på det manuella sättet jag har gjort hittills. Det finns nämligen lysande bibliotek med sådan funktionalitet. Fördelen med sådana bibliotek är både att man helt kan undvika sådana komplicerade funktioner i sin kod, men kanske främst att risken för buggar som är stor med egenbyggda funktioner är betydligt mindre i vältestade bibliotek som många använder. Ett exempel på lysande bibliotek är date-fns.
Så där, nu kan du allt om matematiken bakom veckonummer och skottår!
