@escape-game-over/atlas 0.1.98 → 0.1.100
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/hours.ts +119 -19
- package/src/index.ts +2 -0
package/package.json
CHANGED
package/src/hours.ts
CHANGED
|
@@ -217,6 +217,97 @@ export function formatHours(
|
|
|
217
217
|
): readonly FormattedHours[] {
|
|
218
218
|
const between = format.between ?? "–";
|
|
219
219
|
const and = format.and ?? ", ";
|
|
220
|
+
const { dayName, time } = printer(format);
|
|
221
|
+
|
|
222
|
+
return groupHours(hours, format.weekStart).map((group) => ({
|
|
223
|
+
days: group.runs.map((run) => runName(run, dayName, between)).join(and),
|
|
224
|
+
weekdays: group.days,
|
|
225
|
+
times: timesOf(group, format.closed, time, between, and),
|
|
226
|
+
}));
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** How to say a week in a sentence: `HoursFormat` without its separators. */
|
|
230
|
+
export type HoursSentenceFormat = Omit<HoursFormat, "and">;
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Each language's semicolon, between one group of days and the next. `Intl`
|
|
234
|
+
* has no list form for it, and it is punctuation rather than copy, so it lives
|
|
235
|
+
* here. A language missing from this table throws rather than borrowing the
|
|
236
|
+
* English one: add it here.
|
|
237
|
+
*/
|
|
238
|
+
const SEMICOLONS: Readonly<Record<string, string>> = {
|
|
239
|
+
ar: "؛ ",
|
|
240
|
+
de: "; ",
|
|
241
|
+
el: "· ",
|
|
242
|
+
en: "; ",
|
|
243
|
+
es: "; ",
|
|
244
|
+
// A narrow no-break space before it, as French sets it.
|
|
245
|
+
fr: "\u202F; ",
|
|
246
|
+
hu: "; ",
|
|
247
|
+
it: "; ",
|
|
248
|
+
pt: "; ",
|
|
249
|
+
ru: "; ",
|
|
250
|
+
sv: "; ",
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
function semicolonFor(locale: string): string {
|
|
254
|
+
const { language } = new Intl.Locale(locale);
|
|
255
|
+
const semicolon = Object.hasOwn(SEMICOLONS, language)
|
|
256
|
+
? SEMICOLONS[language]
|
|
257
|
+
: undefined;
|
|
258
|
+
if (semicolon === undefined) {
|
|
259
|
+
throw new Error(
|
|
260
|
+
`hoursSentence has no semicolon for "${language}" (${locale}). Add it to SEMICOLONS in hours.ts.`
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
return semicolon;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* The week as one sentence, for copy that quotes it: `Monday–Thursday and
|
|
268
|
+
* Sunday 14:00–23:30; Friday–Saturday 14:00–00:45`.
|
|
269
|
+
*
|
|
270
|
+
* Days and times join with the locale's own "and" (`Intl.ListFormat`), so a
|
|
271
|
+
* sentence in Arabic reads `الاثنين–الخميس والأحد` rather than borrowing an
|
|
272
|
+
* English comma; groups with the language's semicolon (`؛` in Arabic). No
|
|
273
|
+
* closing full stop: the copy around it decides that.
|
|
274
|
+
*/
|
|
275
|
+
export function hoursSentence(
|
|
276
|
+
hours: WeeklyHours,
|
|
277
|
+
format: HoursSentenceFormat
|
|
278
|
+
): string {
|
|
279
|
+
const between = format.between ?? "–";
|
|
280
|
+
const semicolon = semicolonFor(format.locale);
|
|
281
|
+
const { dayName, time } = printer(format);
|
|
282
|
+
const list = new Intl.ListFormat(format.locale, {
|
|
283
|
+
type: "conjunction",
|
|
284
|
+
style: "long",
|
|
285
|
+
});
|
|
286
|
+
const joined = (parts: readonly string[]) => list.format(parts);
|
|
287
|
+
|
|
288
|
+
return groupHours(hours, format.weekStart)
|
|
289
|
+
.map((group) => {
|
|
290
|
+
const days = joined(
|
|
291
|
+
group.runs.map((run) => runName(run, dayName, between))
|
|
292
|
+
);
|
|
293
|
+
const times =
|
|
294
|
+
group.hours === "closed"
|
|
295
|
+
? format.closed
|
|
296
|
+
: joined(
|
|
297
|
+
group.hours.map(
|
|
298
|
+
(range) =>
|
|
299
|
+
`${time(range.opens)}${between}${time(range.closes)}`
|
|
300
|
+
)
|
|
301
|
+
);
|
|
302
|
+
return `${days} ${times}`;
|
|
303
|
+
})
|
|
304
|
+
.join(semicolon);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** Day names and times as `format` prints them. */
|
|
308
|
+
function printer(
|
|
309
|
+
format: Pick<HoursFormat, "locale" | "weekday" | "hourCycle">
|
|
310
|
+
) {
|
|
220
311
|
// 2024-01-01 was a Monday, so `WEEK[i]` falls on the 1st plus `i`.
|
|
221
312
|
const names = new Intl.DateTimeFormat(format.locale, {
|
|
222
313
|
weekday: format.weekday ?? "short",
|
|
@@ -247,26 +338,35 @@ export function formatHours(
|
|
|
247
338
|
)
|
|
248
339
|
)
|
|
249
340
|
);
|
|
341
|
+
return { dayName, time };
|
|
342
|
+
}
|
|
250
343
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
344
|
+
/** A run of one day prints as one name — `Sun`, not `Sun–Sun`. */
|
|
345
|
+
function runName(
|
|
346
|
+
[first, last]: HoursGroup["runs"][number],
|
|
347
|
+
dayName: (day: Weekday) => string,
|
|
348
|
+
between: string
|
|
349
|
+
): string {
|
|
350
|
+
return first === last
|
|
351
|
+
? dayName(first)
|
|
352
|
+
: `${dayName(first)}${between}${dayName(last)}`;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function timesOf(
|
|
356
|
+
group: HoursGroup,
|
|
357
|
+
closed: string,
|
|
358
|
+
time: (at: TimeOfDay) => string,
|
|
359
|
+
between: string,
|
|
360
|
+
and: string
|
|
361
|
+
): string {
|
|
362
|
+
return group.hours === "closed"
|
|
363
|
+
? closed
|
|
364
|
+
: group.hours
|
|
365
|
+
.map(
|
|
366
|
+
(range) =>
|
|
367
|
+
`${time(range.opens)}${between}${time(range.closes)}`
|
|
368
|
+
)
|
|
369
|
+
.join(and);
|
|
270
370
|
}
|
|
271
371
|
|
|
272
372
|
/** Minutes since midnight, for comparing two times. */
|