|
1 | 1 | import type { HassConfig } from "home-assistant-js-websocket"; |
2 | | -import memoizeOne from "memoize-one"; |
| 2 | +import { format } from "date-fns"; |
| 3 | +import { TZDate } from "@date-fns/tz"; |
3 | 4 | import type { FrontendLocaleData } from "../../data/translation"; |
4 | 5 | import { DateFormat } from "../../data/translation"; |
5 | 6 | import { resolveTimeZone } from "./resolve-time-zone"; |
6 | 7 |
|
| 8 | +// Helper to get date in target timezone |
| 9 | +const toTimeZone = (date: Date, timeZone: string): Date => { |
| 10 | + try { |
| 11 | + return new TZDate(date, timeZone); |
| 12 | + } catch { |
| 13 | + return date; |
| 14 | + } |
| 15 | +}; |
| 16 | + |
| 17 | +// Helper to get format string based on date preference |
| 18 | +const formatForDatePreference = ( |
| 19 | + template: { DMY: string; MDY: string; YMD: string }, |
| 20 | + locale: FrontendLocaleData |
| 21 | +): string => { |
| 22 | + if ( |
| 23 | + locale.date_format === DateFormat.language || |
| 24 | + locale.date_format === DateFormat.system |
| 25 | + ) { |
| 26 | + return template.MDY; // Default to MDY for browser locale |
| 27 | + } |
| 28 | + |
| 29 | + const pattern = |
| 30 | + template[locale.date_format as unknown as keyof typeof template]; |
| 31 | + return pattern || template.MDY; // Fallback to MDY if not found |
| 32 | +}; |
| 33 | + |
7 | 34 | // Tuesday, August 10 |
8 | 35 | export const formatDateWeekdayDay = ( |
9 | 36 | dateObj: Date, |
10 | 37 | locale: FrontendLocaleData, |
11 | 38 | config: HassConfig |
12 | | -) => formatDateWeekdayDayMem(locale, config.time_zone).format(dateObj); |
13 | | - |
14 | | -const formatDateWeekdayDayMem = memoizeOne( |
15 | | - (locale: FrontendLocaleData, serverTimeZone: string) => |
16 | | - new Intl.DateTimeFormat(locale.language, { |
17 | | - weekday: "long", |
18 | | - month: "long", |
19 | | - day: "numeric", |
20 | | - timeZone: resolveTimeZone(locale.time_zone, serverTimeZone), |
21 | | - }) |
22 | | -); |
| 39 | +) => { |
| 40 | + const timeZone = resolveTimeZone(locale.time_zone, config.time_zone); |
| 41 | + const zonedDate = toTimeZone(dateObj, timeZone); |
| 42 | + const pattern = formatForDatePreference( |
| 43 | + { DMY: "EEEE, d MMMM", MDY: "EEEE, MMMM d", YMD: "EEEE, MMMM d" }, |
| 44 | + locale |
| 45 | + ); |
| 46 | + return format(zonedDate, pattern); |
| 47 | +}; |
23 | 48 |
|
24 | 49 | // August 10, 2021 |
25 | 50 | export const formatDate = ( |
26 | 51 | dateObj: Date, |
27 | 52 | locale: FrontendLocaleData, |
28 | 53 | config: HassConfig |
29 | | -) => formatDateMem(locale, config.time_zone).format(dateObj); |
30 | | - |
31 | | -const formatDateMem = memoizeOne( |
32 | | - (locale: FrontendLocaleData, serverTimeZone: string) => |
33 | | - new Intl.DateTimeFormat(locale.language, { |
34 | | - year: "numeric", |
35 | | - month: "long", |
36 | | - day: "numeric", |
37 | | - timeZone: resolveTimeZone(locale.time_zone, serverTimeZone), |
38 | | - }) |
39 | | -); |
| 54 | +) => { |
| 55 | + const timeZone = resolveTimeZone(locale.time_zone, config.time_zone); |
| 56 | + const zonedDate = toTimeZone(dateObj, timeZone); |
| 57 | + const pattern = formatForDatePreference( |
| 58 | + { |
| 59 | + DMY: "d MMMM, yyyy", |
| 60 | + MDY: "MMMM d, yyyy", |
| 61 | + YMD: "yyyy, MMMM d", |
| 62 | + }, |
| 63 | + locale |
| 64 | + ); |
| 65 | + return format(zonedDate, pattern); |
| 66 | +}; |
40 | 67 |
|
41 | 68 | // Aug 10, 2021 |
42 | 69 | export const formatDateShort = ( |
43 | 70 | dateObj: Date, |
44 | 71 | locale: FrontendLocaleData, |
45 | 72 | config: HassConfig |
46 | | -) => formatDateShortMem(locale, config.time_zone).format(dateObj); |
47 | | - |
48 | | -const formatDateShortMem = memoizeOne( |
49 | | - (locale: FrontendLocaleData, serverTimeZone: string) => |
50 | | - new Intl.DateTimeFormat(locale.language, { |
51 | | - year: "numeric", |
52 | | - month: "short", |
53 | | - day: "numeric", |
54 | | - timeZone: resolveTimeZone(locale.time_zone, serverTimeZone), |
55 | | - }) |
56 | | -); |
| 73 | +) => { |
| 74 | + const timeZone = resolveTimeZone(locale.time_zone, config.time_zone); |
| 75 | + const zonedDate = toTimeZone(dateObj, timeZone); |
| 76 | + const pattern = formatForDatePreference( |
| 77 | + { |
| 78 | + DMY: "d MMM, yyyy", |
| 79 | + MDY: "MMM d, yyyy", |
| 80 | + YMD: "yyyy, MMM d", |
| 81 | + }, |
| 82 | + locale |
| 83 | + ); |
| 84 | + return format(zonedDate, pattern); |
| 85 | +}; |
57 | 86 |
|
58 | 87 | // 10/08/2021 |
59 | 88 | export const formatDateNumeric = ( |
60 | 89 | dateObj: Date, |
61 | 90 | locale: FrontendLocaleData, |
62 | 91 | config: HassConfig |
63 | 92 | ) => { |
64 | | - const formatter = formatDateNumericMem(locale, config.time_zone); |
| 93 | + const formatter = createDateNumericFormatter(locale, config.time_zone); |
65 | 94 |
|
66 | 95 | if ( |
67 | 96 | locale.date_format === DateFormat.language || |
@@ -93,193 +122,142 @@ export const formatDateNumeric = ( |
93 | 122 | return formats[locale.date_format]; |
94 | 123 | }; |
95 | 124 |
|
96 | | -const formatDateNumericMem = memoizeOne( |
97 | | - (locale: FrontendLocaleData, serverTimeZone: string) => { |
98 | | - const localeString = |
99 | | - locale.date_format === DateFormat.system ? undefined : locale.language; |
100 | | - |
101 | | - if ( |
102 | | - locale.date_format === DateFormat.language || |
103 | | - locale.date_format === DateFormat.system |
104 | | - ) { |
105 | | - return new Intl.DateTimeFormat(localeString, { |
106 | | - year: "numeric", |
107 | | - month: "numeric", |
108 | | - day: "numeric", |
109 | | - timeZone: resolveTimeZone(locale.time_zone, serverTimeZone), |
110 | | - }); |
111 | | - } |
112 | | - |
113 | | - return new Intl.DateTimeFormat(localeString, { |
114 | | - year: "numeric", |
115 | | - month: "numeric", |
116 | | - day: "numeric", |
117 | | - timeZone: resolveTimeZone(locale.time_zone, serverTimeZone), |
118 | | - }); |
119 | | - } |
120 | | -); |
| 125 | +const createDateNumericFormatter = ( |
| 126 | + locale: FrontendLocaleData, |
| 127 | + serverTimeZone: string |
| 128 | +) => { |
| 129 | + const localeString = |
| 130 | + locale.date_format === DateFormat.system ? undefined : locale.language; |
| 131 | + return new Intl.DateTimeFormat(localeString, { |
| 132 | + year: "numeric", |
| 133 | + month: "numeric", |
| 134 | + day: "numeric", |
| 135 | + timeZone: resolveTimeZone(locale.time_zone, serverTimeZone), |
| 136 | + }); |
| 137 | +}; |
121 | 138 |
|
122 | 139 | // Aug 10 |
123 | 140 | export const formatDateVeryShort = ( |
124 | 141 | dateObj: Date, |
125 | 142 | locale: FrontendLocaleData, |
126 | 143 | config: HassConfig |
127 | | -) => formatDateVeryShortMem(locale, config.time_zone).format(dateObj); |
128 | | - |
129 | | -const formatDateVeryShortMem = memoizeOne( |
130 | | - (locale: FrontendLocaleData, serverTimeZone: string) => |
131 | | - new Intl.DateTimeFormat(locale.language, { |
132 | | - day: "numeric", |
133 | | - month: "short", |
134 | | - timeZone: resolveTimeZone(locale.time_zone, serverTimeZone), |
135 | | - }) |
136 | | -); |
| 144 | +) => { |
| 145 | + const timeZone = resolveTimeZone(locale.time_zone, config.time_zone); |
| 146 | + const zonedDate = toTimeZone(dateObj, timeZone); |
| 147 | + const pattern = formatForDatePreference( |
| 148 | + { DMY: "d MMM", MDY: "MMM d", YMD: "MMM d" }, |
| 149 | + locale |
| 150 | + ); |
| 151 | + return format(zonedDate, pattern); |
| 152 | +}; |
137 | 153 |
|
138 | 154 | // August 2021 |
139 | 155 | export const formatDateMonthYear = ( |
140 | 156 | dateObj: Date, |
141 | 157 | locale: FrontendLocaleData, |
142 | 158 | config: HassConfig |
143 | | -) => formatDateMonthYearMem(locale, config.time_zone).format(dateObj); |
144 | | - |
145 | | -const formatDateMonthYearMem = memoizeOne( |
146 | | - (locale: FrontendLocaleData, serverTimeZone: string) => |
147 | | - new Intl.DateTimeFormat(locale.language, { |
148 | | - month: "long", |
149 | | - year: "numeric", |
150 | | - timeZone: resolveTimeZone(locale.time_zone, serverTimeZone), |
151 | | - }) |
152 | | -); |
| 159 | +) => { |
| 160 | + const timeZone = resolveTimeZone(locale.time_zone, config.time_zone); |
| 161 | + const zonedDate = toTimeZone(dateObj, timeZone); |
| 162 | + const pattern = formatForDatePreference( |
| 163 | + { |
| 164 | + DMY: "MMMM yyyy", |
| 165 | + MDY: "MMMM yyyy", |
| 166 | + YMD: "yyyy MMMM", |
| 167 | + }, |
| 168 | + locale |
| 169 | + ); |
| 170 | + return format(zonedDate, pattern); |
| 171 | +}; |
153 | 172 |
|
154 | 173 | // August |
155 | 174 | export const formatDateMonth = ( |
156 | 175 | dateObj: Date, |
157 | 176 | locale: FrontendLocaleData, |
158 | 177 | config: HassConfig |
159 | | -) => formatDateMonthMem(locale, config.time_zone).format(dateObj); |
160 | | - |
161 | | -const formatDateMonthMem = memoizeOne( |
162 | | - (locale: FrontendLocaleData, serverTimeZone: string) => |
163 | | - new Intl.DateTimeFormat(locale.language, { |
164 | | - month: "long", |
165 | | - timeZone: resolveTimeZone(locale.time_zone, serverTimeZone), |
166 | | - }) |
167 | | -); |
| 178 | +) => { |
| 179 | + const timeZone = resolveTimeZone(locale.time_zone, config.time_zone); |
| 180 | + const zonedDate = toTimeZone(dateObj, timeZone); |
| 181 | + return format(zonedDate, "MMMM"); |
| 182 | +}; |
168 | 183 |
|
169 | 184 | // Aug |
170 | 185 | export const formatDateMonthShort = ( |
171 | 186 | dateObj: Date, |
172 | 187 | locale: FrontendLocaleData, |
173 | 188 | config: HassConfig |
174 | | -) => formatDateMonthShortMem(locale, config.time_zone).format(dateObj); |
175 | | - |
176 | | -const formatDateMonthShortMem = memoizeOne( |
177 | | - (locale: FrontendLocaleData, serverTimeZone: string) => |
178 | | - new Intl.DateTimeFormat(locale.language, { |
179 | | - month: "short", |
180 | | - timeZone: resolveTimeZone(locale.time_zone, serverTimeZone), |
181 | | - }) |
182 | | -); |
| 189 | +) => { |
| 190 | + const timeZone = resolveTimeZone(locale.time_zone, config.time_zone); |
| 191 | + const zonedDate = toTimeZone(dateObj, timeZone); |
| 192 | + return format(zonedDate, "MMM"); |
| 193 | +}; |
183 | 194 |
|
184 | 195 | // 2021 |
185 | 196 | export const formatDateYear = ( |
186 | 197 | dateObj: Date, |
187 | 198 | locale: FrontendLocaleData, |
188 | 199 | config: HassConfig |
189 | | -) => formatDateYearMem(locale, config.time_zone).format(dateObj); |
190 | | - |
191 | | -const formatDateYearMem = memoizeOne( |
192 | | - (locale: FrontendLocaleData, serverTimeZone: string) => |
193 | | - new Intl.DateTimeFormat(locale.language, { |
194 | | - year: "numeric", |
195 | | - timeZone: resolveTimeZone(locale.time_zone, serverTimeZone), |
196 | | - }) |
197 | | -); |
| 200 | +) => { |
| 201 | + const timeZone = resolveTimeZone(locale.time_zone, config.time_zone); |
| 202 | + const zonedDate = toTimeZone(dateObj, timeZone); |
| 203 | + return format(zonedDate, "yyyy"); |
| 204 | +}; |
198 | 205 |
|
199 | 206 | // Monday |
200 | 207 | export const formatDateWeekday = ( |
201 | 208 | dateObj: Date, |
202 | 209 | locale: FrontendLocaleData, |
203 | 210 | config: HassConfig |
204 | | -) => formatDateWeekdayMem(locale, config.time_zone).format(dateObj); |
205 | | - |
206 | | -const formatDateWeekdayMem = memoizeOne( |
207 | | - (locale: FrontendLocaleData, serverTimeZone: string) => |
208 | | - new Intl.DateTimeFormat(locale.language, { |
209 | | - weekday: "long", |
210 | | - timeZone: resolveTimeZone(locale.time_zone, serverTimeZone), |
211 | | - }) |
212 | | -); |
| 211 | +) => { |
| 212 | + const timeZone = resolveTimeZone(locale.time_zone, config.time_zone); |
| 213 | + const zonedDate = toTimeZone(dateObj, timeZone); |
| 214 | + return format(zonedDate, "EEEE"); |
| 215 | +}; |
213 | 216 |
|
214 | 217 | // Mon |
215 | 218 | export const formatDateWeekdayShort = ( |
216 | 219 | dateObj: Date, |
217 | 220 | locale: FrontendLocaleData, |
218 | 221 | config: HassConfig |
219 | | -) => formatDateWeekdayShortMem(locale, config.time_zone).format(dateObj); |
220 | | - |
221 | | -const formatDateWeekdayShortMem = memoizeOne( |
222 | | - (locale: FrontendLocaleData, serverTimeZone: string) => |
223 | | - new Intl.DateTimeFormat(locale.language, { |
224 | | - weekday: "short", |
225 | | - timeZone: resolveTimeZone(locale.time_zone, serverTimeZone), |
226 | | - }) |
227 | | -); |
| 222 | +) => { |
| 223 | + const timeZone = resolveTimeZone(locale.time_zone, config.time_zone); |
| 224 | + const zonedDate = toTimeZone(dateObj, timeZone); |
| 225 | + return format(zonedDate, "EEE"); |
| 226 | +}; |
228 | 227 |
|
229 | 228 | // Mon, Aug 10 |
230 | 229 | export const formatDateWeekdayVeryShortDate = ( |
231 | 230 | dateObj: Date, |
232 | 231 | locale: FrontendLocaleData, |
233 | 232 | config: HassConfig |
234 | | -) => |
235 | | - formatDateWeekdayVeryShortDateMem(locale, config.time_zone).format(dateObj); |
236 | | - |
237 | | -const formatDateWeekdayVeryShortDateMem = memoizeOne( |
238 | | - (locale: FrontendLocaleData, serverTimeZone: string) => |
239 | | - new Intl.DateTimeFormat(locale.language, { |
240 | | - weekday: "short", |
241 | | - month: "short", |
242 | | - day: "numeric", |
243 | | - timeZone: resolveTimeZone(locale.time_zone, serverTimeZone), |
244 | | - }) |
245 | | -); |
| 233 | +) => { |
| 234 | + const timeZone = resolveTimeZone(locale.time_zone, config.time_zone); |
| 235 | + const zonedDate = toTimeZone(dateObj, timeZone); |
| 236 | + return format(zonedDate, "EEE, MMM d"); |
| 237 | +}; |
246 | 238 |
|
247 | 239 | // Mon, Aug 10, 2021 |
248 | 240 | export const formatDateWeekdayShortDate = ( |
249 | 241 | dateObj: Date, |
250 | 242 | locale: FrontendLocaleData, |
251 | 243 | config: HassConfig |
252 | | -) => formatDateWeekdayShortDateMem(locale, config.time_zone).format(dateObj); |
253 | | - |
254 | | -const formatDateWeekdayShortDateMem = memoizeOne( |
255 | | - (locale: FrontendLocaleData, serverTimeZone: string) => |
256 | | - new Intl.DateTimeFormat(locale.language, { |
257 | | - weekday: "short", |
258 | | - month: "short", |
259 | | - day: "numeric", |
260 | | - year: "numeric", |
261 | | - timeZone: resolveTimeZone(locale.time_zone, serverTimeZone), |
262 | | - }) |
263 | | -); |
| 244 | +) => { |
| 245 | + const timeZone = resolveTimeZone(locale.time_zone, config.time_zone); |
| 246 | + const zonedDate = toTimeZone(dateObj, timeZone); |
| 247 | + return format(zonedDate, "EEE, MMM d, yyyy"); |
| 248 | +}; |
264 | 249 |
|
265 | 250 | /** |
266 | | - * Format a date as YYYY-MM-DD. Uses "en-CA" because it's the only |
267 | | - * Intl locale that natively outputs ISO 8601 date format. |
268 | | - * Locale/config are only used to resolve the time zone. |
| 251 | + * Format a date as YYYY-MM-DD |
269 | 252 | */ |
270 | 253 | export const formatISODateOnly = ( |
271 | 254 | dateObj: Date, |
272 | 255 | locale: FrontendLocaleData, |
273 | 256 | config: HassConfig |
274 | 257 | ) => { |
275 | 258 | const timeZone = resolveTimeZone(locale.time_zone, config.time_zone); |
276 | | - const formatter = new Intl.DateTimeFormat("en-CA", { |
277 | | - year: "numeric", |
278 | | - month: "2-digit", |
279 | | - day: "2-digit", |
280 | | - timeZone, |
281 | | - }); |
282 | | - return formatter.format(dateObj); |
| 259 | + const zonedDate = toTimeZone(dateObj, timeZone); |
| 260 | + return format(zonedDate, "yyyy-MM-dd"); |
283 | 261 | }; |
284 | 262 |
|
285 | 263 | // 2026-08-10/2026-08-15 |
|
0 commit comments