Födelsedagsmatematik
Har du någonsin undrat hur gammal du är ... i millisekunder? Eller exakt hur många veckor det är mellan två tidpunkter? Lugn, lugn – i det här inlägget kan du få svar på alla dina "Hur många [enhet] är det mellan [tidpunkt] och [tidpunkt]?"-frågor!
TL;DR
- Millisekunder, sekunder, minuter, timmar, dagar och veckor mellan två tidpunkter är trivialt att räkna ut – det är bara att dela millisekundsdifferensen med rätt konstant.
- Månader och år är knepigare eftersom deras längd varierar. Tricket är att räkna hela perioder framåt från startdatumet och addera hur långt in i nästa period vi har kommit – samma ankarbaserade modell som Temporal-API:n använder.
- Vill du bara ha svaret kan du anropa
Temporal.Duration.prototype.totaldirekt i stället för att skriva uträkningarna själv.
Testa modellen här:
Till exempel din födelsedagTill exempel dagens datum
31,841325196728
Månader382,102789893519
Veckor1661,440528115
Dagar11630,083696806
Timmar279122,008723
Minuter16747320,523400
Sekunder1004839231,404
Millisekunder1004839231404
Användning
Välj start- och slutdatum ovan och få svar på allt du någonsin har undrat över! Startdatumet är min födelsedag och slutdatumet är dagens datum innan du ändrar något. Som du ser uppdateras värdena hela tiden, och det gör de så länge slutdatumet är dagens datum. Om du väljer ett annat slutdatum slutar uppdateringarna.
Vid valet av startdatum finns även möjligheten att ange klockslag genom att kryssa i "Ange även klockslag". På så sätt kan du till exempel ange ditt födelsedatum med klockslag för att ta reda på exakt hur många minuter gammal du är. När "Ange även klockslag" inte är ifylld antas klockslaget 00:00:00.000, och när klockslag är valt anges timmar och minuter medan sekunder och millisekunder antas vara 0. Sekunder och millisekunder i resultatet är extra intressanta eftersom de räknar upp i realtid när slutdatumet är dagens datum – där syns det verkligen att tiden tickar.
Date-objektet
I JavaScript finns det inbyggda Date-objektet som innehåller massor av bra information och därför är bra att använda i den här typen av beräkningar. Med new Date() skapas ett nytt Date-objekt med information om nuvarande millisekund, som kan tas fram med hjälp av en rad olika funktioner som hör till Date-objektet. Här är några exempel för datumet 2020-10-12:
const now = new Date()
const currentYear = now.getFullYear()
// currentYear = 2020
const currentMonth = now.getMonth()
// currentMonth = 9
const currentDate = now.getDate()
// currentDate = 12
const currentTime = now.getTime()
// currentTime = 1602486314645
Två av dessa funktioner som kan verka lite märkliga är getMonth och getTime. Trots att det är 2020-10-12 blir currentMonth 9. Det beror på att månaderna räknas från 0. Januari är alltså 0 och så vidare till och med december som är månad 11. getTime skulle man kunna tro skulle vara ett klockslag i stil med "09:05", men returnerar i stället något så konstigt som antalet millisekunder från midnatt 1970-01-01. Det är JavaScripts strategi för att ange en exakt tidpunkt. Alla tider före 1970-01-01 representeras alltså i ett negativt tal om getTime används.
Beräkningarna
Den triviala biten: alla enheter med fast längd kan räknas ut genom att dela millisekundsdifferensen med rätt konstant.
const MS_PER_SECOND = 1000
const MS_PER_MINUTE = MS_PER_SECOND * 60
const MS_PER_HOUR = MS_PER_MINUTE * 60
const MS_PER_DAY = MS_PER_HOUR * 24
const MS_PER_WEEK = MS_PER_DAY * 7
function getMillisecondsDiff(startDate, endDate) {
return endDate.getTime() - startDate.getTime()
}
function getSecondsDiff(startDate, endDate) {
return getMillisecondsDiff(startDate, endDate) / MS_PER_SECOND
}
function getMinutesDiff(startDate, endDate) {
return getMillisecondsDiff(startDate, endDate) / MS_PER_MINUTE
}
function getHoursDiff(startDate, endDate) {
return getMillisecondsDiff(startDate, endDate) / MS_PER_HOUR
}
function getDaysDiff(startDate, endDate) {
return getMillisecondsDiff(startDate, endDate) / MS_PER_DAY
}
function getWeeksDiff(startDate, endDate) {
return getMillisecondsDiff(startDate, endDate) / MS_PER_WEEK
}
Alla funktioner följer samma mönster – byt bara ut konstanten i nämnaren. Tack vare millisekundsupplösningen får vi gratis "rörliga" decimaler som tickar uppåt i realtid.
Att räkna ut exakt hur många månader eller år det är mellan två Date-objekt ska däremot visa sig vara betydligt svårare.
Månader mellan tidpunkter
Innan vi går in på beräkningar i kod måste vi fundera över hur en månad definieras. Eftersom en månads längd varierar, till skillnad från dagar per vecka och minuter per timme och liknande, är det inte självklart hur många månader 10 dagar är. Ska en genomsnittlig månads längd användas? Nja – det är både klurigt att räkna ut med tanke på komplexiteten kring skottår och det skulle också resultera i konstigheter som att 1–15 februari skulle räknas som mindre än en månad även om mer än hälften av den aktuella månaden har passerat.
I stället använder vi en ankarbaserad modell – samma som Temporal-API:n använder:
- Räkna hur många hela månader som ryms när vi går framåt från startdatumet, månad för månad.
- Sätt ett ankare = startdatumet plus så många månader.
- Bråkdelen blir hur långt vi har kommit från ankaret mot nästa ankare (en månad till), dividerat med längden på just den perioden.
Låt mig förklara vad jag menar med några exempel.
Exempel 1Startdatum: 2018-01-01 kl. 00:00
Slutdatum: 2020-02-01 kl. 00:00
Hela 2018 och 2019 ger 24 månader, och januari 2020 ger en till. Det blir 25 hela månader, och eftersom slutdatumet ligger exakt på nästa ankare (2020-02-01) är bråkdelen 0. Totalt: 25 månader.
Exempel 2Startdatum: 2018-04-01 kl. 00:00
Slutdatum: 2020-02-01 kl. 00:00
Från april 2018 ryms 22 hela månader fram till februari 2020. Återigen ligger slutdatumet exakt på ankarpunkten, så bråkdelen är 0. Totalt: 22 månader.
Exempel 3Startdatum: 2018-04-24 kl. 00:00
Slutdatum: 2020-02-06 kl. 00:00
Från 2018-04-24 kan vi gå framåt månad för månad: 2018-05-24, 2018-06-24, …, hela vägen till 2020-01-24. Nästa ankare skulle bli 2020-02-24, men det ligger efter slutdatumet, så där stannar vi. Vi har alltså 21 hela månader, och ankaret ligger på 2020-01-24.
Bråkdelen är hur långt vi har kommit från ankaret (2020-01-24) mot nästa ankare (2020-02-24). Slutdatumet 2020-02-06 ligger 13 dagar efter 2020-01-24, och hela perioden 2020-01-24 → 2020-02-24 är 31 dagar lång. Alltså 13/31 ≈ 0,4193548 månad. Totalt: 21,4193548 månader.
Att addera månader är knepigare än det låter
För att räkna ut ankaret behöver vi en funktion som lägger till n månader till ett datum. Mer knepigt än det låter, eftersom månader har olika längd:
- 2018-01-31 + 1 månad = ? Februari har inte 31 dagar. Standardlösningen är att klampa till sista dagen i målmånaden: 2018-02-28.
- 2020-01-31 + 1 månad = 2020-02-29 (skottår!).
- 2020-02-29 + 12 månader = 2021-02-28 (klampning igen).
JavaScripts inbyggda setMonth har dessutom en lurig egenhet: new Date(2018, 0, 31) följt av setMonth(1) blir 2018-03-03, inte 2018-02-28, eftersom JavaScript "rullar över" dagarna i stället för att klampa. Vi kan komma runt det genom att först sätta dagen till 1, sedan ändra månaden och slutligen sätta dagen igen med klampning:
function addMonths(date, n) {
const result = new Date(date)
result.setDate(1) // Undvik day-overflow när vi byter månad
result.setMonth(date.getMonth() + n)
const daysInResultMonth = new Date(
result.getFullYear(),
result.getMonth() + 1,
0,
).getDate()
result.setDate(Math.min(date.getDate(), daysInResultMonth))
return result
}
Den näst sista raden använder day 0-tricket: new Date(year, month + 1, 0) ser ut som "den 0:e dagen i nästa månad", men JavaScript normaliserar det till sista dagen i den aktuella månaden. Smidigt sätt att få antalet dagar i en månad utan att själv tänka på skottår.
Modellen i kod
Med addMonths på plats kan getMonthsDiff skrivas så här:
function getMonthsDiff(startDate, endDate) {
let months =
(endDate.getFullYear() - startDate.getFullYear()) * 12 +
(endDate.getMonth() - startDate.getMonth())
if (addMonths(startDate, months) > endDate) months -= 1
const anchor = addMonths(startDate, months)
const nextAnchor = addMonths(startDate, months + 1)
const fractional =
(endDate.getTime() - anchor.getTime()) /
(nextAnchor.getTime() - anchor.getTime())
return months + fractional
}
Första uttrycket räknar skillnaden i kalendermånader mellan slut och start, vilket är ett maxvärde för antalet hela månader. Om vi har "övergått" – det vill säga om ankaret med så många månader hamnar efter slutdatumet (som i Exempel 3, där 2020-02-24 ligger efter 2020-02-06) – drar vi av en månad. Sedan delar vi avståndet från ankaret till slutdatumet med längden av nästa hela månadsperiod.
Vi kan nu testa getMonthsDiff med Exempel 3:
const startDate = new Date("2018-04-24 00:00")
const endDate = new Date("2020-02-06 00:00")
const monthsDiff = getMonthsDiff(startDate, endDate)
// monthsDiff = 21.419354838709676
Stämmer med kontrollräkningen ovan (21 + 13/31).
År mellan tidpunkter
År räknas med precis samma ankarmodell – det är ju i grunden tolv månader åt gången:
function getYearsDiff(startDate, endDate) {
let years = endDate.getFullYear() - startDate.getFullYear()
if (addMonths(startDate, years * 12) > endDate) years -= 1
const anchor = addMonths(startDate, years * 12)
const nextAnchor = addMonths(startDate, (years + 1) * 12)
const fractional =
(endDate.getTime() - anchor.getTime()) /
(nextAnchor.getTime() - anchor.getTime())
return years + fractional
}
Testar vi med ett enkelt fall blir det som väntat:
const startDate = new Date("2018-01-01")
const endDate = new Date("2020-01-01")
const yearsDiff = getYearsDiff(startDate, endDate)
// yearsDiff = 2
Och med ett lite mer komplicerat fall:
const startDate = new Date("2018-05-03")
const endDate = new Date("2020-03-22")
const yearsDiff = getYearsDiff(startDate, endDate)
// yearsDiff = 1.8852459016393444
Kontrollräkning: 1 helt år ryms från 2018-05-03 till 2019-05-03. Nästa ankare (2020-05-03) ligger efter slutdatumet, så där stannar vi. Bråkdelen blir (2020-03-22 − 2019-05-03) / (2020-05-03 − 2019-05-03) = 324 / 366 = 0,8852459. Totalt: 1,8852459 år.
Allt tillsammans
Hela funktionaliteten ser alltså ut så här:
const MS_PER_SECOND = 1000
const MS_PER_MINUTE = MS_PER_SECOND * 60
const MS_PER_HOUR = MS_PER_MINUTE * 60
const MS_PER_DAY = MS_PER_HOUR * 24
const MS_PER_WEEK = MS_PER_DAY * 7
function getMillisecondsDiff(startDate, endDate) {
return endDate.getTime() - startDate.getTime()
}
function getSecondsDiff(startDate, endDate) {
return getMillisecondsDiff(startDate, endDate) / MS_PER_SECOND
}
function getMinutesDiff(startDate, endDate) {
return getMillisecondsDiff(startDate, endDate) / MS_PER_MINUTE
}
function getHoursDiff(startDate, endDate) {
return getMillisecondsDiff(startDate, endDate) / MS_PER_HOUR
}
function getDaysDiff(startDate, endDate) {
return getMillisecondsDiff(startDate, endDate) / MS_PER_DAY
}
function getWeeksDiff(startDate, endDate) {
return getMillisecondsDiff(startDate, endDate) / MS_PER_WEEK
}
function addMonths(date, n) {
const result = new Date(date)
result.setDate(1)
result.setMonth(date.getMonth() + n)
const daysInResultMonth = new Date(
result.getFullYear(),
result.getMonth() + 1,
0,
).getDate()
result.setDate(Math.min(date.getDate(), daysInResultMonth))
return result
}
function getMonthsDiff(startDate, endDate) {
let months =
(endDate.getFullYear() - startDate.getFullYear()) * 12 +
(endDate.getMonth() - startDate.getMonth())
if (addMonths(startDate, months) > endDate) months -= 1
const anchor = addMonths(startDate, months)
const nextAnchor = addMonths(startDate, months + 1)
const fractional =
(endDate.getTime() - anchor.getTime()) /
(nextAnchor.getTime() - anchor.getTime())
return months + fractional
}
function getYearsDiff(startDate, endDate) {
let years = endDate.getFullYear() - startDate.getFullYear()
if (addMonths(startDate, years * 12) > endDate) years -= 1
const anchor = addMonths(startDate, years * 12)
const nextAnchor = addMonths(startDate, (years + 1) * 12)
const fractional =
(endDate.getTime() - anchor.getTime()) /
(nextAnchor.getTime() - anchor.getTime())
return years + fractional
}
Bättre alternativ
Modellen ovan är i grunden en handgjord version av Temporals Duration.prototype.total. I ny kod skulle jag bara använda Temporal-API:n direkt:
const start = Temporal.PlainDateTime.from("1994-08-06T10:00")
const end = Temporal.Now.plainDateTimeISO()
// Decimaler i en specifik enhet
const years = end.since(start).total({ unit: "year", relativeTo: start })
const months = end.since(start).total({ unit: "month", relativeTo: start })
const weeks = end.since(start).total({ unit: "week" })
// Eller som heltalsuppdelning: { years, months, days, hours, ... }
const duration = end.since(start, { largestUnit: "year" })
Temporal är ett relativt nytt tillägg, så stödet varierar fortfarande mellan miljöer. Där den inte finns fungerar polyfillen @js-temporal/polyfill.
Ett etablerat bibliotek som date-fns har också funktioner som differenceInYears, differenceInMonths och differenceInWeeks, men de returnerar bara heltal – vill du ha decimaler får du blanda flera anrop eller räkna på millisekunder själv.
Den manuella implementationen i den här artikeln blir alltså i praktiken överflödig – men förhoppningsvis pedagogiskt nyttig för att förstå varför det inte alltid är trivialt att räkna ut hur många månader eller år det är mellan två tidpunkter.
Så där, jag kan skryta med att vara 1661,440528163 veckor gammal och du kan själv se hur gammal du är – exakt.
Referenser
- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date
- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/getTime
- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal
- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Temporal/Duration
- https://date-fns.org/
