From 5e4b67375149595d93e6357996f24e6696039c0e Mon Sep 17 00:00:00 2001 From: Adam Kapos Date: Tue, 24 Jan 2023 14:53:57 +0200 Subject: [PATCH] Fix relative time above 22 hours (#15072) fixes https://github.com/home-assistant/frontend/issues/14815 fixes undefined --- src/common/datetime/relative_time.ts | 2 +- src/common/util/select-unit.ts | 50 +++- test/common/datetime/relative_time.ts | 401 ++++++++++++++++++++------ 3 files changed, 357 insertions(+), 96 deletions(-) diff --git a/src/common/datetime/relative_time.ts b/src/common/datetime/relative_time.ts index ffe4f8ce49..7b8e6f2e6b 100644 --- a/src/common/datetime/relative_time.ts +++ b/src/common/datetime/relative_time.ts @@ -18,7 +18,7 @@ export const relativeTime = ( to?: Date, includeTense = true ): string => { - const diff = selectUnit(from, to); + const diff = selectUnit(from, to, locale); if (includeTense) { return formatRelTimeMem(locale).format(diff.value, diff.unit); } diff --git a/src/common/util/select-unit.ts b/src/common/util/select-unit.ts index e18845cd44..f68599c610 100644 --- a/src/common/util/select-unit.ts +++ b/src/common/util/select-unit.ts @@ -1,3 +1,7 @@ +import { differenceInDays, differenceInWeeks, startOfWeek } from "date-fns/esm"; +import { FrontendLocaleData } from "../../data/translation"; +import { firstWeekdayIndex } from "../datetime/first_weekday"; + export type Unit = | "second" | "minute" @@ -11,13 +15,12 @@ export type Unit = const MS_PER_SECOND = 1e3; const SECS_PER_MIN = 60; const SECS_PER_HOUR = SECS_PER_MIN * 60; -const SECS_PER_DAY = SECS_PER_HOUR * 24; -const SECS_PER_WEEK = SECS_PER_DAY * 7; // Adapted from https://github.com/formatjs/formatjs/blob/186cef62f980ec66252ee232f438a42d0b51b9f9/packages/intl-utils/src/diff.ts export function selectUnit( from: Date | number, to: Date | number = Date.now(), + locale: FrontendLocaleData, thresholds: Partial = {} ): { value: number; unit: Unit } { const resolvedThresholds: Thresholds = { @@ -49,29 +52,56 @@ export function selectUnit( }; } - const days = secs / SECS_PER_DAY; + const fromDate = new Date(from); + const toDate = new Date(to); + + // Set time component to zero, which allows us to compare only the days + fromDate.setHours(0, 0, 0, 0); + toDate.setHours(0, 0, 0, 0); + + const days = differenceInDays(fromDate, toDate); + if (days === 0) { + return { + value: Math.round(hours), + unit: "hour", + }; + } if (Math.abs(days) < resolvedThresholds.day) { return { - value: Math.round(days), + value: days, unit: "day", }; } - const weeks = secs / SECS_PER_WEEK; + const firstWeekday = firstWeekdayIndex(locale); + const fromWeek = startOfWeek(fromDate, { weekStartsOn: firstWeekday }); + const toWeek = startOfWeek(toDate, { weekStartsOn: firstWeekday }); + + const weeks = differenceInWeeks(fromWeek, toWeek); + if (weeks === 0) { + return { + value: days, + unit: "day", + }; + } if (Math.abs(weeks) < resolvedThresholds.week) { return { - value: Math.round(weeks), + value: weeks, unit: "week", }; } - const fromDate = new Date(from); - const toDate = new Date(to); const years = fromDate.getFullYear() - toDate.getFullYear(); const months = years * 12 + fromDate.getMonth() - toDate.getMonth(); - if (Math.round(Math.abs(months)) < resolvedThresholds.month) { + if (months === 0) { return { - value: Math.round(months), + value: weeks, + unit: "week", + }; + } + if (Math.abs(months) < resolvedThresholds.month || years === 0) { + return { + value: months, unit: "month", }; } diff --git a/test/common/datetime/relative_time.ts b/test/common/datetime/relative_time.ts index b91a0bef92..c7b4f87fc6 100644 --- a/test/common/datetime/relative_time.ts +++ b/test/common/datetime/relative_time.ts @@ -15,114 +15,345 @@ describe("relativeTime", () => { first_weekday: FirstWeekday.language, }; - it("now", () => { + const locale_monday = { + language: "en", + number_format: NumberFormat.language, + time_format: TimeFormat.language, + first_weekday: FirstWeekday.monday, + }; + + describe("no time difference", () => { const now = new Date(); - assert.strictEqual(relativeTime(now, locale, now), "now"); - assert.strictEqual(relativeTime(now, locale, now, false), "0 seconds"); + it("returns now with tense", () => { + assert.strictEqual(relativeTime(now, locale, now), "now"); + }); + it("returns 0 seconds without tense", () => { + assert.strictEqual(relativeTime(now, locale, now, false), "0 seconds"); + }); }); - it("past_second", () => { - const inputdt = new Date("2021-02-03T11:22:00+00:00"); - const compare = new Date("2021-02-03T11:22:33+00:00"); - assert.strictEqual( - relativeTime(inputdt, locale, compare), - "33 seconds ago" - ); - assert.strictEqual( - relativeTime(inputdt, locale, compare, false), - "33 seconds" - ); + describe("33 second difference", () => { + const date1 = new Date("2021-02-03T11:22:00+00:00"); + const date2 = new Date("2021-02-03T11:22:33+00:00"); + + it("past tense", () => { + assert.strictEqual(relativeTime(date1, locale, date2), "33 seconds ago"); + }); + + it("future tense", () => { + assert.strictEqual(relativeTime(date2, locale, date1), "in 33 seconds"); + }); + + it("without tense", () => { + assert.strictEqual( + relativeTime(date1, locale, date2, false), + "33 seconds" + ); + + assert.strictEqual( + relativeTime(date2, locale, date1, false), + "33 seconds" + ); + }); }); - it("past_minute", () => { - const inputdt = new Date("2021-02-03T11:20:33+00:00"); - const compare = new Date("2021-02-03T11:22:33+00:00"); - assert.strictEqual(relativeTime(inputdt, locale, compare), "2 minutes ago"); - assert.strictEqual( - relativeTime(inputdt, locale, compare, false), - "2 minutes" - ); + describe("2 minute difference", () => { + const date1 = new Date("2021-02-03T11:20:33+00:00"); + const date2 = new Date("2021-02-03T11:22:33+00:00"); + + it("past tense", () => { + assert.strictEqual(relativeTime(date1, locale, date2), "2 minutes ago"); + }); + + it("future tense", () => { + assert.strictEqual(relativeTime(date2, locale, date1), "in 2 minutes"); + }); + + it("without tense", () => { + assert.strictEqual( + relativeTime(date1, locale, date2, false), + "2 minutes" + ); + + assert.strictEqual( + relativeTime(date2, locale, date1, false), + "2 minutes" + ); + }); }); - it("past_hour", () => { - const inputdt = new Date("2021-02-03T09:22:33+00:00"); - const compare = new Date("2021-02-03T11:22:33+00:00"); - assert.strictEqual(relativeTime(inputdt, locale, compare), "2 hours ago"); - assert.strictEqual( - relativeTime(inputdt, locale, compare, false), - "2 hours" - ); + describe("2 hour difference", () => { + const date1 = new Date("2021-02-03T09:22:33+00:00"); + const date2 = new Date("2021-02-03T11:22:33+00:00"); + + it("past tense", () => { + assert.strictEqual(relativeTime(date1, locale, date2), "2 hours ago"); + }); + + it("future tense", () => { + assert.strictEqual(relativeTime(date2, locale, date1), "in 2 hours"); + }); + + it("without tense", () => { + assert.strictEqual(relativeTime(date1, locale, date2, false), "2 hours"); + + assert.strictEqual(relativeTime(date2, locale, date1, false), "2 hours"); + }); }); - it("past_day", () => { - const inputdt = new Date("2021-02-01T11:22:33+00:00"); - const compare = new Date("2021-02-03T11:22:33+00:00"); - assert.strictEqual(relativeTime(inputdt, locale, compare), "2 days ago"); - assert.strictEqual(relativeTime(inputdt, locale, compare, false), "2 days"); + describe("23 hour difference during the same day", () => { + const date1 = new Date("2021-02-01T00:22:33+00:00"); + const date2 = new Date("2021-02-01T23:22:33+00:00"); + + it("past tense", () => { + assert.strictEqual(relativeTime(date1, locale, date2), "23 hours ago"); + }); + + it("future tense", () => { + assert.strictEqual(relativeTime(date2, locale, date1), "in 23 hours"); + }); + + it("without tense", () => { + assert.strictEqual(relativeTime(date1, locale, date2, false), "23 hours"); + + assert.strictEqual(relativeTime(date2, locale, date1, false), "23 hours"); + }); }); - it("future_second", () => { - const inputdt = new Date("2021-02-03T11:22:55+00:00"); - const compare = new Date("2021-02-03T11:22:33+00:00"); - assert.strictEqual(relativeTime(inputdt, locale, compare), "in 22 seconds"); - assert.strictEqual( - relativeTime(inputdt, locale, compare, false), - "22 seconds" - ); + describe("23 hour difference during different days", () => { + const date1 = new Date("2021-02-01T11:22:33+00:00"); + const date2 = new Date("2021-02-02T10:22:33+00:00"); + + it("past tense", () => { + assert.strictEqual(relativeTime(date1, locale, date2), "yesterday"); + }); + + it("future tense", () => { + assert.strictEqual(relativeTime(date2, locale, date1), "tomorrow"); + }); + + it("without tense", () => { + assert.strictEqual(relativeTime(date1, locale, date2, false), "1 day"); + + assert.strictEqual(relativeTime(date2, locale, date1, false), "1 day"); + }); }); - it("future_minute", () => { - const inputdt = new Date("2021-02-03T11:24:33+00:00"); - const compare = new Date("2021-02-03T11:22:33+00:00"); - assert.strictEqual(relativeTime(inputdt, locale, compare), "in 2 minutes"); - assert.strictEqual( - relativeTime(inputdt, locale, compare, false), - "2 minutes" - ); + describe("33 hour difference during three days", () => { + const date1 = new Date("2021-02-01T21:22:33+00:00"); + const date2 = new Date("2021-02-03T06:22:33+00:00"); + + it("past tense", () => { + assert.strictEqual(relativeTime(date1, locale, date2), "2 days ago"); + }); + + it("future tense", () => { + assert.strictEqual(relativeTime(date2, locale, date1), "in 2 days"); + }); + + it("without tense", () => { + assert.strictEqual(relativeTime(date1, locale, date2, false), "2 days"); + + assert.strictEqual(relativeTime(date2, locale, date1, false), "2 days"); + }); }); - it("future_hour", () => { - const inputdt = new Date("2021-02-03T13:22:33+00:00"); - const compare = new Date("2021-02-03T11:22:33+00:00"); - assert.strictEqual(relativeTime(inputdt, locale, compare), "in 2 hours"); - assert.strictEqual( - relativeTime(inputdt, locale, compare, false), - "2 hours" - ); + describe("5 day difference Sunday to Friday", () => { + const date1 = new Date("2021-01-31T20:22:33+00:00"); + const date2 = new Date("2021-02-05T21:22:33+00:00"); + + describe("with Sunday as first day of the week", () => { + it("past tense", () => { + assert.strictEqual(relativeTime(date1, locale, date2), "5 days ago"); + }); + + it("future tense", () => { + assert.strictEqual(relativeTime(date2, locale, date1), "in 5 days"); + }); + + it("without tense", () => { + assert.strictEqual(relativeTime(date1, locale, date2, false), "5 days"); + + assert.strictEqual(relativeTime(date2, locale, date1, false), "5 days"); + }); + }); + + describe("with Monday as first day of the week", () => { + it("past tense", () => { + assert.strictEqual( + relativeTime(date1, locale_monday, date2), + "last week" + ); + }); + + it("future tense", () => { + assert.strictEqual( + relativeTime(date2, locale_monday, date1), + "next week" + ); + }); + + it("without tense", () => { + assert.strictEqual( + relativeTime(date1, locale_monday, date2, false), + "1 week" + ); + + assert.strictEqual( + relativeTime(date2, locale_monday, date1, false), + "1 week" + ); + }); + }); }); - it("future_day", () => { - const inputdt = new Date("2021-02-05T11:22:33+00:00"); - const compare = new Date("2021-02-03T11:22:33+00:00"); - assert.strictEqual(relativeTime(inputdt, locale, compare), "in 2 days"); - assert.strictEqual(relativeTime(inputdt, locale, compare, false), "2 days"); + describe("5 day difference Tuesday to Sunday", () => { + const date1 = new Date("2021-02-02T20:22:33+00:00"); + const date2 = new Date("2021-02-07T21:22:33+00:00"); + + describe("with Sunday as first day of the week", () => { + it("past tense", () => { + assert.strictEqual(relativeTime(date1, locale, date2), "last week"); + }); + + it("future tense", () => { + assert.strictEqual(relativeTime(date2, locale, date1), "next week"); + }); + + it("without tense", () => { + assert.strictEqual(relativeTime(date1, locale, date2, false), "1 week"); + + assert.strictEqual(relativeTime(date2, locale, date1, false), "1 week"); + }); + }); + + describe("with Monday as first day of the week", () => { + it("past tense", () => { + assert.strictEqual( + relativeTime(date1, locale_monday, date2), + "5 days ago" + ); + }); + + it("future tense", () => { + assert.strictEqual( + relativeTime(date2, locale_monday, date1), + "in 5 days" + ); + }); + + it("without tense", () => { + assert.strictEqual( + relativeTime(date1, locale_monday, date2, false), + "5 days" + ); + + assert.strictEqual( + relativeTime(date2, locale_monday, date1, false), + "5 days" + ); + }); + }); }); - it("future_week", () => { - const inputdt = new Date("2021-03-24T11:22:33+00:00"); - const compare = new Date("2021-03-03T11:22:33+00:00"); - assert.strictEqual(relativeTime(inputdt, locale, compare), "in 3 weeks"); - assert.strictEqual( - relativeTime(inputdt, locale, compare, false), - "3 weeks" - ); + describe("11 day difference during three weeks", () => { + const date1 = new Date("2021-02-05T20:22:33+00:00"); + const date2 = new Date("2021-02-16T21:22:33+00:00"); + + it("past tense", () => { + assert.strictEqual(relativeTime(date1, locale, date2), "2 weeks ago"); + }); + + it("future tense", () => { + assert.strictEqual(relativeTime(date2, locale, date1), "in 2 weeks"); + }); + + it("without tense", () => { + assert.strictEqual(relativeTime(date1, locale, date2, false), "2 weeks"); + + assert.strictEqual(relativeTime(date2, locale, date1, false), "2 weeks"); + }); }); - it("future_month", () => { - const inputdt = new Date("2021-03-03T11:22:33+00:00"); - const compare = new Date("2021-02-03T11:22:33+00:00"); - assert.strictEqual(relativeTime(inputdt, locale, compare), "next month"); - assert.strictEqual( - relativeTime(inputdt, locale, compare, false), - "1 month" - ); + describe("30 day difference during the same month", () => { + const date1 = new Date("2021-03-01T20:22:33+00:00"); + const date2 = new Date("2021-03-31T21:22:33+00:00"); + + it("past tense", () => { + assert.strictEqual(relativeTime(date1, locale, date2), "4 weeks ago"); + }); + + it("future tense", () => { + assert.strictEqual(relativeTime(date2, locale, date1), "in 4 weeks"); + }); + + it("without tense", () => { + assert.strictEqual(relativeTime(date1, locale, date2, false), "4 weeks"); + + assert.strictEqual(relativeTime(date2, locale, date1, false), "4 weeks"); + }); }); - it("handles a jump between years", () => { - const inputdt = new Date("2021-12-29"); - const compare = new Date("2022-01-01"); + describe("30 day difference during different months", () => { + const date1 = new Date("2021-02-05T20:22:33+00:00"); + const date2 = new Date("2021-03-07T21:22:33+00:00"); - assert.strictEqual(relativeTime(inputdt, locale, compare), "3 days ago"); - assert.strictEqual(relativeTime(inputdt, locale, compare, false), "3 days"); + it("past tense", () => { + assert.strictEqual(relativeTime(date1, locale, date2), "last month"); + }); + + it("future tense", () => { + assert.strictEqual(relativeTime(date2, locale, date1), "next month"); + }); + + it("without tense", () => { + assert.strictEqual(relativeTime(date1, locale, date2, false), "1 month"); + + assert.strictEqual(relativeTime(date2, locale, date1, false), "1 month"); + }); + }); + + describe("11 month difference during same year", () => { + const date1 = new Date("2021-01-05T20:22:33+00:00"); + const date2 = new Date("2021-12-05T21:22:33+00:00"); + + it("past tense", () => { + assert.strictEqual(relativeTime(date1, locale, date2), "11 months ago"); + }); + + it("future tense", () => { + assert.strictEqual(relativeTime(date2, locale, date1), "in 11 months"); + }); + + it("without tense", () => { + assert.strictEqual( + relativeTime(date1, locale, date2, false), + "11 months" + ); + + assert.strictEqual( + relativeTime(date2, locale, date1, false), + "11 months" + ); + }); + }); + + describe("11 month difference during different years", () => { + const date1 = new Date("2021-02-05T20:22:33+00:00"); + const date2 = new Date("2022-01-05T21:22:33+00:00"); + + it("past tense", () => { + assert.strictEqual(relativeTime(date1, locale, date2), "last year"); + }); + + it("future tense", () => { + assert.strictEqual(relativeTime(date2, locale, date1), "next year"); + }); + + it("without tense", () => { + assert.strictEqual(relativeTime(date1, locale, date2, false), "1 year"); + + assert.strictEqual(relativeTime(date2, locale, date1, false), "1 year"); + }); }); });