@escape-game-over/atlas 0.1.98 → 0.1.99

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@escape-game-over/atlas",
3
- "version": "0.1.98",
3
+ "version": "0.1.99",
4
4
  "type": "module",
5
5
  "description": "Typed, data-driven machinery for static multi-locale, multi-deployment Astro sites.",
6
6
  "private": false,
package/src/hours.ts CHANGED
@@ -217,6 +217,71 @@ 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
+ /**
230
+ * How to say a week in a sentence: `HoursFormat` without the separators it
231
+ * picks from `Intl`, plus the one `Intl` has no word for.
232
+ */
233
+ export interface HoursSentenceFormat extends Omit<HoursFormat, "and"> {
234
+ /**
235
+ * Between one group of days and the next: `"; "` in English, `"؛ "` in
236
+ * Arabic. Copy, so the caller translates it; `Intl` has no list form
237
+ * for it.
238
+ */
239
+ readonly separator: string;
240
+ }
241
+
242
+ /**
243
+ * The week as one sentence, for copy that quotes it: `Monday–Thursday and
244
+ * Sunday 14:00–23:30; Friday–Saturday 14:00–00:45`.
245
+ *
246
+ * Days and times join with the locale's own "and" (`Intl.ListFormat`), so a
247
+ * sentence in Arabic reads `الاثنين–الخميس والأحد` rather than borrowing an
248
+ * English comma. No closing full stop: the copy around it decides that.
249
+ */
250
+ export function hoursSentence(
251
+ hours: WeeklyHours,
252
+ format: HoursSentenceFormat
253
+ ): string {
254
+ const between = format.between ?? "–";
255
+ const { dayName, time } = printer(format);
256
+ const list = new Intl.ListFormat(format.locale, {
257
+ type: "conjunction",
258
+ style: "long",
259
+ });
260
+ const joined = (parts: readonly string[]) => list.format(parts);
261
+
262
+ return groupHours(hours, format.weekStart)
263
+ .map((group) => {
264
+ const days = joined(
265
+ group.runs.map((run) => runName(run, dayName, between))
266
+ );
267
+ const times =
268
+ group.hours === "closed"
269
+ ? format.closed
270
+ : joined(
271
+ group.hours.map(
272
+ (range) =>
273
+ `${time(range.opens)}${between}${time(range.closes)}`
274
+ )
275
+ );
276
+ return `${days} ${times}`;
277
+ })
278
+ .join(format.separator);
279
+ }
280
+
281
+ /** Day names and times as `format` prints them. */
282
+ function printer(
283
+ format: Pick<HoursFormat, "locale" | "weekday" | "hourCycle">
284
+ ) {
220
285
  // 2024-01-01 was a Monday, so `WEEK[i]` falls on the 1st plus `i`.
221
286
  const names = new Intl.DateTimeFormat(format.locale, {
222
287
  weekday: format.weekday ?? "short",
@@ -247,26 +312,35 @@ export function formatHours(
247
312
  )
248
313
  )
249
314
  );
315
+ return { dayName, time };
316
+ }
250
317
 
251
- return groupHours(hours, format.weekStart).map((group) => ({
252
- days: group.runs
253
- .map(([first, last]) =>
254
- first === last
255
- ? dayName(first)
256
- : `${dayName(first)}${between}${dayName(last)}`
257
- )
258
- .join(and),
259
- weekdays: group.days,
260
- times:
261
- group.hours === "closed"
262
- ? format.closed
263
- : group.hours
264
- .map(
265
- (range) =>
266
- `${time(range.opens)}${between}${time(range.closes)}`
267
- )
268
- .join(and),
269
- }));
318
+ /** A run of one day prints as one name — `Sun`, not `Sun–Sun`. */
319
+ function runName(
320
+ [first, last]: HoursGroup["runs"][number],
321
+ dayName: (day: Weekday) => string,
322
+ between: string
323
+ ): string {
324
+ return first === last
325
+ ? dayName(first)
326
+ : `${dayName(first)}${between}${dayName(last)}`;
327
+ }
328
+
329
+ function timesOf(
330
+ group: HoursGroup,
331
+ closed: string,
332
+ time: (at: TimeOfDay) => string,
333
+ between: string,
334
+ and: string
335
+ ): string {
336
+ return group.hours === "closed"
337
+ ? closed
338
+ : group.hours
339
+ .map(
340
+ (range) =>
341
+ `${time(range.opens)}${between}${time(range.closes)}`
342
+ )
343
+ .join(and);
270
344
  }
271
345
 
272
346
  /** Minutes since midnight, for comparing two times. */
package/src/index.ts CHANGED
@@ -123,6 +123,8 @@ export {
123
123
  groupHours,
124
124
  type HoursFormat,
125
125
  type HoursGroup,
126
+ type HoursSentenceFormat,
127
+ hoursSentence,
126
128
  type OpenStatus,
127
129
  openStatus,
128
130
  type TimeOfDay,