@asteby/storefront 0.3.0 → 0.4.0
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/README.md +51 -0
- package/dist/chunk-45R47HYO.js +144 -0
- package/dist/chunk-45R47HYO.js.map +1 -0
- package/dist/geo.cjs +154 -0
- package/dist/geo.cjs.map +1 -0
- package/dist/geo.d.cts +82 -0
- package/dist/geo.d.ts +82 -0
- package/dist/geo.js +3 -0
- package/dist/geo.js.map +1 -0
- package/dist/index.cjs +20 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +22 -3
- package/dist/index.d.ts +22 -3
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -1
- package/dist/mock.cjs +115 -7
- package/dist/mock.cjs.map +1 -1
- package/dist/mock.d.cts +8 -2
- package/dist/mock.d.ts +8 -2
- package/dist/mock.js +93 -8
- package/dist/mock.js.map +1 -1
- package/dist/{types-CTXB8Dnl.d.cts → types-BmC-RDo8.d.ts} +51 -1
- package/dist/{types-CTXB8Dnl.d.ts → types-D300wdPF.d.cts} +51 -1
- package/package.json +11 -1
package/README.md
CHANGED
|
@@ -126,6 +126,57 @@ Images are `https://picsum.photos/seed/<handle>/800/800` placeholders built by
|
|
|
126
126
|
`MOCK_IMAGE_URL`; pass `createMockStorefront({ imageUrl })` to swap them.
|
|
127
127
|
`mock.reset()` restores the seed and `mock.state` exposes the live data.
|
|
128
128
|
|
|
129
|
+
## Geo helpers (`@asteby/storefront/geo`)
|
|
130
|
+
|
|
131
|
+
Pure TypeScript (no network, no DOM; Node 20, edge and browsers). Behaviour is
|
|
132
|
+
pinned by `src/geo-vectors.json`, shared with the Go implementation.
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
import {
|
|
136
|
+
distanceKm, rankByDistance, formatDistance, directionsUrl, parseLatLng,
|
|
137
|
+
openStatus, formatOpeningHours, toSchemaOpeningHours,
|
|
138
|
+
} from '@asteby/storefront/geo'
|
|
139
|
+
|
|
140
|
+
const origin = parseLatLng('19.4326,-99.1332') // null when malformed or (0,0)
|
|
141
|
+
const near = rankByDistance(shop.branches, origin!, { radiusKm: 25, limit: 3 })
|
|
142
|
+
// [{ item, distanceKm }] sorted by distance, ties keep input order.
|
|
143
|
+
// Items without a valid point (null, out of range or the "unset" (0,0)) are excluded.
|
|
144
|
+
formatDistance(near[0]!.distanceKm, 'es-MX') // "300 m" (50 m steps) or "4.2 km"
|
|
145
|
+
directionsUrl(near[0]!.item, { provider: 'google' }) // coordinates, else address text
|
|
146
|
+
|
|
147
|
+
const hours = { weekly: [{ day: 1, open: '09:00', close: '19:00' }, /* 1 = Mon ... 7 = Sun */] }
|
|
148
|
+
openStatus(hours, 'America/Mexico_City') // { open, closesAt, nextOpenAt } as ISO instants
|
|
149
|
+
formatOpeningHours(hours, 'es-MX') // "Lun–Vie 9:00–19:00 · Sáb 9:00–14:00"
|
|
150
|
+
toSchemaOpeningHours(hours) // ["Mo-Fr 09:00-19:00", "Sa 09:00-14:00"]
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
`openStatus` honors intervals that cross midnight, several intervals per day,
|
|
154
|
+
dated `exceptions` and daylight saving time (uses `Intl.DateTimeFormat`; throws
|
|
155
|
+
`RangeError` for an unknown time zone). `ShopBranch` has optional
|
|
156
|
+
`opening_hours`, `timezone` and `opening_hours_text`; they arrive with Storefront
|
|
157
|
+
API `locations.v1` and are not populated yet.
|
|
158
|
+
|
|
159
|
+
## Locations (`client.locations`, Storefront API `locations.v1`)
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
const { nodes } = await sf.locations.list() // visible branches
|
|
163
|
+
const byCp = await sf.locations.list({ postal_code: '06000' }) // ordered by distance_km
|
|
164
|
+
const one = await sf.locations.get('br_centro') // null when unknown
|
|
165
|
+
const near = await sf.locations.nearest({ lat: 19.4326, lng: -99.1332 }) // closest first, nodes[i].distance_km
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
`Location` is a `ShopBranch` whose structured fields are always present: `kind`,
|
|
169
|
+
`postal_code`, `timezone`, `opening_hours`, `services`, `contacts` and
|
|
170
|
+
`pickup { enabled, fee, prep_days }` (`fee: null` = free). `nearest` is
|
|
171
|
+
`private, no-store`: the server rounds the point to 0.01° and never stores it,
|
|
172
|
+
and the client neither logs nor keeps it (hooks such as `onRequest` do see the
|
|
173
|
+
URL, so do not log it). To rank branches you already have without sending the
|
|
174
|
+
point anywhere, use `rankByDistance` from `/geo` on `shop.branches`.
|
|
175
|
+
The mock serves the three routes with three example branches (CDMX, Monterrey,
|
|
176
|
+
Guadalajara) ordered by haversine; it knows the centroid of a few postal codes
|
|
177
|
+
(`MOCK_POSTAL_CENTROIDS`), pickup is offered only where `pickup.enabled`, and
|
|
178
|
+
the pickup `fee` is charged on the cart.
|
|
179
|
+
|
|
129
180
|
## API
|
|
130
181
|
|
|
131
182
|
`createStorefrontClient(options)`:
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// src/geo.ts
|
|
2
|
+
var RAD = Math.PI / 180;
|
|
3
|
+
function distanceKm(a, b) {
|
|
4
|
+
const dLat = (b.lat - a.lat) * RAD;
|
|
5
|
+
const dLng = (b.lng - a.lng) * RAD;
|
|
6
|
+
const h = Math.sin(dLat / 2) ** 2 + Math.cos(a.lat * RAD) * Math.cos(b.lat * RAD) * Math.sin(dLng / 2) ** 2;
|
|
7
|
+
return 2 * 6371.0088 * Math.atan2(Math.sqrt(h), Math.sqrt(1 - h));
|
|
8
|
+
}
|
|
9
|
+
function isValidPoint(p) {
|
|
10
|
+
const { lat, lng } = p ?? {};
|
|
11
|
+
return Number.isFinite(lat) && Number.isFinite(lng) && Math.abs(lat) <= 90 && Math.abs(lng) <= 180 && !(lat === 0 && lng === 0);
|
|
12
|
+
}
|
|
13
|
+
function parseLatLng(s) {
|
|
14
|
+
const [a, b, ...rest] = (s ?? "").split(",");
|
|
15
|
+
const p = !rest.length && a?.trim() && b?.trim() ? { lat: Number(a), lng: Number(b) } : null;
|
|
16
|
+
return isValidPoint(p) ? p : null;
|
|
17
|
+
}
|
|
18
|
+
function rankByDistance(items, origin, { radiusKm, limit, getPoint = (i) => i } = {}) {
|
|
19
|
+
const out = [];
|
|
20
|
+
for (const item of items) {
|
|
21
|
+
const p = getPoint(item);
|
|
22
|
+
if (!isValidPoint(p)) continue;
|
|
23
|
+
const d = distanceKm(origin, p);
|
|
24
|
+
if (radiusKm === void 0 || d <= radiusKm) out.push({ item, distanceKm: d });
|
|
25
|
+
}
|
|
26
|
+
out.sort((x, y) => x.distanceKm - y.distanceKm);
|
|
27
|
+
return limit === void 0 ? out : out.slice(0, Math.max(0, limit));
|
|
28
|
+
}
|
|
29
|
+
function formatDistance(km, locale = "es-MX") {
|
|
30
|
+
const m = Math.round(km * 1e3 / 50) * 50;
|
|
31
|
+
const [value, unit, digits] = m < 1e3 ? [m, "meter", 0] : [km, "kilometer", 1];
|
|
32
|
+
return new Intl.NumberFormat(locale, {
|
|
33
|
+
style: "unit",
|
|
34
|
+
unit,
|
|
35
|
+
minimumFractionDigits: digits,
|
|
36
|
+
maximumFractionDigits: digits
|
|
37
|
+
}).format(value);
|
|
38
|
+
}
|
|
39
|
+
function directionsUrl(location, { provider = "google" } = {}) {
|
|
40
|
+
const ll = isValidPoint(location) ? `${location.lat},${location.lng}` : null;
|
|
41
|
+
const text = [location.address, location.city].filter(Boolean).join(", ");
|
|
42
|
+
const dest = ll ?? text;
|
|
43
|
+
if (!dest) return null;
|
|
44
|
+
const q = encodeURIComponent(dest);
|
|
45
|
+
if (provider === "apple") return `https://maps.apple.com/?daddr=${q}`;
|
|
46
|
+
if (provider === "geo") return ll ? `geo:${ll}?q=${ll}` : `geo:0,0?q=${q}`;
|
|
47
|
+
return `https://www.google.com/maps/dir/?api=1&destination=${q}`;
|
|
48
|
+
}
|
|
49
|
+
var toMin = (t) => {
|
|
50
|
+
const m = /^(\d{1,2}):(\d{2})$/.exec(t ?? "");
|
|
51
|
+
return m ? Number(m[1]) * 60 + Number(m[2]) : NaN;
|
|
52
|
+
};
|
|
53
|
+
var interval = (open, close) => {
|
|
54
|
+
const s = toMin(open);
|
|
55
|
+
let e = toMin(close);
|
|
56
|
+
if (Number.isNaN(s) || Number.isNaN(e)) return [];
|
|
57
|
+
if (e <= s) e += 1440;
|
|
58
|
+
return [[s, e]];
|
|
59
|
+
};
|
|
60
|
+
var dayIntervals = (h, day, date) => {
|
|
61
|
+
const x = h.exceptions?.find((e) => e.date === date);
|
|
62
|
+
if (x?.closed) return [];
|
|
63
|
+
if (x && x.open && x.close) return interval(x.open, x.close);
|
|
64
|
+
return h.weekly.filter((w) => w.day === day).flatMap((w) => interval(w.open, w.close));
|
|
65
|
+
};
|
|
66
|
+
function openStatus(hours, timeZone, now = /* @__PURE__ */ new Date()) {
|
|
67
|
+
const fmt = new Intl.DateTimeFormat("en-US", {
|
|
68
|
+
timeZone,
|
|
69
|
+
hourCycle: "h23",
|
|
70
|
+
year: "numeric",
|
|
71
|
+
month: "numeric",
|
|
72
|
+
day: "numeric",
|
|
73
|
+
hour: "numeric",
|
|
74
|
+
minute: "numeric"
|
|
75
|
+
});
|
|
76
|
+
const wall = (ms) => {
|
|
77
|
+
const p = {};
|
|
78
|
+
for (const { type, value } of fmt.formatToParts(ms)) p[type] = Number(value);
|
|
79
|
+
return Date.UTC(p.year, p.month - 1, p.day, (p.hour ?? 0) % 24, p.minute) / 6e4;
|
|
80
|
+
};
|
|
81
|
+
const nowMs = now.getTime();
|
|
82
|
+
const w = wall(nowMs);
|
|
83
|
+
const day0 = Math.floor(w / 1440) * 1440;
|
|
84
|
+
const offset = (ms) => wall(ms) - Math.floor(ms / 6e4);
|
|
85
|
+
const toInstant = (t) => {
|
|
86
|
+
const guess = (t - offset(nowMs)) * 6e4;
|
|
87
|
+
return new Date((t - offset(guess)) * 6e4).toISOString();
|
|
88
|
+
};
|
|
89
|
+
const all = [];
|
|
90
|
+
for (let d = -1; d <= 8; d++) {
|
|
91
|
+
const at = new Date((day0 + d * 1440) * 6e4);
|
|
92
|
+
const day = at.getUTCDay() || 7;
|
|
93
|
+
for (const [s, e] of dayIntervals(hours, day, at.toISOString().slice(0, 10))) all.push([day0 + d * 1440 + s, day0 + d * 1440 + e]);
|
|
94
|
+
}
|
|
95
|
+
all.sort((a, b) => a[0] - b[0]);
|
|
96
|
+
const merged = [];
|
|
97
|
+
for (const iv of all) {
|
|
98
|
+
const last = merged[merged.length - 1];
|
|
99
|
+
if (last && iv[0] <= last[1]) last[1] = Math.max(last[1], iv[1]);
|
|
100
|
+
else merged.push([iv[0], iv[1]]);
|
|
101
|
+
}
|
|
102
|
+
const current = merged.find(([s, e]) => s <= w && w < e);
|
|
103
|
+
if (current) return { open: true, closesAt: toInstant(current[1]), nextOpenAt: null };
|
|
104
|
+
const next = merged.find(([s]) => s > w);
|
|
105
|
+
return { open: false, closesAt: null, nextOpenAt: next ? toInstant(next[0]) : null };
|
|
106
|
+
}
|
|
107
|
+
var runs = (h) => {
|
|
108
|
+
const out = [];
|
|
109
|
+
for (let day = 1; day <= 7; day++) {
|
|
110
|
+
const iv = h.weekly.filter((x) => x.day === day).flatMap((x) => interval(x.open, x.close)).sort((a, b) => a[0] - b[0]);
|
|
111
|
+
if (!iv.length) continue;
|
|
112
|
+
const last = out[out.length - 1];
|
|
113
|
+
if (last && last.to === day - 1 && JSON.stringify(last.iv) === JSON.stringify(iv)) last.to = day;
|
|
114
|
+
else out.push({ from: day, to: day, iv });
|
|
115
|
+
}
|
|
116
|
+
return out;
|
|
117
|
+
};
|
|
118
|
+
var hm = (min) => [min && min % 1440 === 0 ? 24 : Math.floor(min / 60) % 24, String(min % 60).padStart(2, "0")];
|
|
119
|
+
function formatOpeningHours(hours, locale = "es-MX") {
|
|
120
|
+
const en = /^en/i.test(locale);
|
|
121
|
+
const name = (d) => {
|
|
122
|
+
const s = new Intl.DateTimeFormat(locale, { weekday: "short", timeZone: "UTC" }).format(Date.UTC(2024, 0, d)).replace(/\.$/, "");
|
|
123
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
124
|
+
};
|
|
125
|
+
const time = (min) => {
|
|
126
|
+
const [h, m] = hm(min);
|
|
127
|
+
return en ? `${h % 12 || 12}:${m} ${h % 24 < 12 ? "AM" : "PM"}` : `${h}:${m}`;
|
|
128
|
+
};
|
|
129
|
+
return runs(hours).map((r) => `${name(r.from)}${r.from === r.to ? "" : "\u2013" + name(r.to)} ${r.iv.map(([s, e]) => `${time(s)}\u2013${time(e)}`).join(", ")}`).join(" \xB7 ");
|
|
130
|
+
}
|
|
131
|
+
function toSchemaOpeningHours(hours) {
|
|
132
|
+
const code = ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"];
|
|
133
|
+
const t = (min) => {
|
|
134
|
+
const [h, m] = hm(min);
|
|
135
|
+
return `${String(h).padStart(2, "0")}:${m}`;
|
|
136
|
+
};
|
|
137
|
+
return runs(hours).flatMap(
|
|
138
|
+
(r) => r.iv.map(([s, e]) => `${code[r.from - 1]}${r.from === r.to ? "" : "-" + code[r.to - 1]} ${t(s)}-${t(e)}`)
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export { directionsUrl, distanceKm, formatDistance, formatOpeningHours, isValidPoint, openStatus, parseLatLng, rankByDistance, toSchemaOpeningHours };
|
|
143
|
+
//# sourceMappingURL=chunk-45R47HYO.js.map
|
|
144
|
+
//# sourceMappingURL=chunk-45R47HYO.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/geo.ts"],"names":[],"mappings":"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","file":"chunk-45R47HYO.js","sourcesContent":["/**\n * Geo helpers: pure TypeScript, no network and no DOM. Works in Node 20, edge\n * runtimes and browsers. Behaviour is pinned by `geo-vectors.json`, which the\n * Go side copies verbatim, so keep both implementations in sync.\n */\n\nexport type GeoPoint = { lat: number; lng: number }\n\n/** `(0, 0)` means \"unset\" on this platform, so it is never a valid point. */\ntype MaybePoint = { lat?: number | null; lng?: number | null } | null | undefined\n\nexport type OpeningHours = {\n /** `day`: 1 = Monday ... 7 = Sunday. Several entries per day are allowed. A `close` at or before `open` crosses midnight. */\n weekly: { day: number; open: string; close: string }[]\n /** Dated overrides of the weekly rule. `closed`, or both `open` and `close`. */\n exceptions?: { date: string; closed?: true; open?: string; close?: string; note?: string }[]\n}\n\nexport type OpenStatus = {\n open: boolean\n /** ISO instant (UTC) when the current interval ends; `null` when closed. */\n closesAt: string | null\n /** ISO instant (UTC) of the next opening; `null` when open or none within 8 days. */\n nextOpenAt: string | null\n}\n\nconst RAD = Math.PI / 180\n\n/** Great-circle distance in km (haversine, earth radius 6371.0088 km). */\nexport function distanceKm(a: GeoPoint, b: GeoPoint): number {\n const dLat = (b.lat - a.lat) * RAD\n const dLng = (b.lng - a.lng) * RAD\n const h = Math.sin(dLat / 2) ** 2 + Math.cos(a.lat * RAD) * Math.cos(b.lat * RAD) * Math.sin(dLng / 2) ** 2\n return 2 * 6371.0088 * Math.atan2(Math.sqrt(h), Math.sqrt(1 - h))\n}\n\n/** Finite, in range and not the `(0, 0)` \"unset\" pair. */\nexport function isValidPoint(p: MaybePoint): p is GeoPoint {\n const { lat, lng } = p ?? {}\n // Number.isFinite is false for null, undefined and non-numbers.\n return Number.isFinite(lat) && Number.isFinite(lng) && Math.abs(lat!) <= 90 && Math.abs(lng!) <= 180 && !(lat === 0 && lng === 0)\n}\n\n/** `'19.43,-99.13'` (spaces allowed) to a point; `null` when malformed or invalid. */\nexport function parseLatLng(s: string | null | undefined): GeoPoint | null {\n const [a, b, ...rest] = (s ?? '').split(',')\n const p = !rest.length && a?.trim() && b?.trim() ? { lat: Number(a), lng: Number(b) } : null\n return isValidPoint(p) ? p : null\n}\n\nexport type RankOptions<T> = {\n /** Keep items at most this far away (inclusive). */\n radiusKm?: number\n /** Keep the first N after sorting. */\n limit?: number\n /** Defaults to reading `lat` / `lng` from the item. */\n getPoint?: (item: T) => MaybePoint\n}\n\n/**\n * Sorts by distance from `origin` (stable: ties keep input order). Items without\n * a valid point are EXCLUDED, not placed last, because they have no distance.\n */\nexport function rankByDistance<T>(\n items: readonly T[],\n origin: GeoPoint,\n { radiusKm, limit, getPoint = (i) => i as MaybePoint }: RankOptions<T> = {},\n): { item: T; distanceKm: number }[] {\n const out: { item: T; distanceKm: number }[] = []\n for (const item of items) {\n const p = getPoint(item)\n if (!isValidPoint(p)) continue\n const d = distanceKm(origin, p)\n if (radiusKm === undefined || d <= radiusKm) out.push({ item, distanceKm: d })\n }\n // Array.prototype.sort is stable (ES2019), so ties keep input order.\n out.sort((x, y) => x.distanceKm - y.distanceKm)\n return limit === undefined ? out : out.slice(0, Math.max(0, limit))\n}\n\n/** Under 1 km: meters rounded to 50 m. Otherwise km with one decimal. */\nexport function formatDistance(km: number, locale = 'es-MX'): string {\n const m = Math.round((km * 1000) / 50) * 50\n const [value, unit, digits] = m < 1000 ? [m, 'meter', 0] : [km, 'kilometer', 1]\n return new Intl.NumberFormat(locale, {\n style: 'unit', unit, minimumFractionDigits: digits, maximumFractionDigits: digits,\n }).format(value)\n}\n\n/** Destination for directions: coordinates when valid, else the address text. */\nexport function directionsUrl(\n location: { lat?: number | null; lng?: number | null; address?: string | null; city?: string | null },\n { provider = 'google' }: { provider?: 'google' | 'apple' | 'geo' } = {},\n): string | null {\n const ll = isValidPoint(location) ? `${location.lat},${location.lng}` : null\n const text = [location.address, location.city].filter(Boolean).join(', ')\n const dest = ll ?? text\n if (!dest) return null\n const q = encodeURIComponent(dest)\n if (provider === 'apple') return `https://maps.apple.com/?daddr=${q}`\n if (provider === 'geo') return ll ? `geo:${ll}?q=${ll}` : `geo:0,0?q=${q}`\n return `https://www.google.com/maps/dir/?api=1&destination=${q}`\n}\n\n// --- opening hours -------------------------------------------------------\n\nconst toMin = (t: string | undefined): number => {\n const m = /^(\\d{1,2}):(\\d{2})$/.exec(t ?? '')\n return m ? Number(m[1]) * 60 + Number(m[2]) : NaN\n}\n\ntype Iv = [number, number]\n\n/** Intervals in minutes; an end at or before the start crosses midnight. */\nconst interval = (open: string | undefined, close: string | undefined): Iv[] => {\n const s = toMin(open)\n let e = toMin(close)\n if (Number.isNaN(s) || Number.isNaN(e)) return []\n if (e <= s) e += 1440\n return [[s, e]]\n}\n\nconst dayIntervals = (h: OpeningHours, day: number, date: string): Iv[] => {\n const x = h.exceptions?.find((e) => e.date === date)\n if (x?.closed) return []\n if (x && x.open && x.close) return interval(x.open, x.close)\n return h.weekly.filter((w) => w.day === day).flatMap((w) => interval(w.open, w.close))\n}\n\n/**\n * Whether the place is open at `now` in the IANA `timeZone` (throws RangeError\n * for an unknown zone). Looks 8 days ahead for the next opening. Handles\n * intervals crossing midnight, several per day, dated exceptions and DST.\n */\nexport function openStatus(hours: OpeningHours, timeZone: string, now: Date = new Date()): OpenStatus {\n const fmt = new Intl.DateTimeFormat('en-US', {\n timeZone, hourCycle: 'h23', year: 'numeric', month: 'numeric', day: 'numeric', hour: 'numeric', minute: 'numeric',\n })\n // Wall-clock minutes since the epoch as seen in `timeZone`.\n const wall = (ms: number): number => {\n const p: Record<string, number> = {}\n for (const { type, value } of fmt.formatToParts(ms)) p[type] = Number(value)\n return Date.UTC(p.year!, p.month! - 1, p.day!, (p.hour ?? 0) % 24, p.minute!) / 60000\n }\n const nowMs = now.getTime()\n const w = wall(nowMs)\n const day0 = Math.floor(w / 1440) * 1440\n // Offset from UTC, in minutes, at an instant.\n const offset = (ms: number): number => wall(ms) - Math.floor(ms / 60000)\n const toInstant = (t: number): string => {\n const guess = (t - offset(nowMs)) * 60000\n return new Date((t - offset(guess)) * 60000).toISOString()\n }\n\n const all: Iv[] = []\n for (let d = -1; d <= 8; d++) {\n const at = new Date((day0 + d * 1440) * 60000)\n const day = at.getUTCDay() || 7\n for (const [s, e] of dayIntervals(hours, day, at.toISOString().slice(0, 10))) all.push([day0 + d * 1440 + s, day0 + d * 1440 + e])\n }\n all.sort((a, b) => a[0] - b[0])\n const merged: Iv[] = []\n for (const iv of all) {\n const last = merged[merged.length - 1]\n if (last && iv[0] <= last[1]) last[1] = Math.max(last[1], iv[1])\n else merged.push([iv[0], iv[1]])\n }\n const current = merged.find(([s, e]) => s <= w && w < e)\n if (current) return { open: true, closesAt: toInstant(current[1]), nextOpenAt: null }\n const next = merged.find(([s]) => s > w)\n return { open: false, closesAt: null, nextOpenAt: next ? toInstant(next[0]) : null }\n}\n\ntype Run = { from: number; to: number; iv: Iv[] }\n\n/** Consecutive days (1..7) with identical intervals, in week order. */\nconst runs = (h: OpeningHours): Run[] => {\n const out: Run[] = []\n for (let day = 1; day <= 7; day++) {\n const iv = h.weekly.filter((x) => x.day === day).flatMap((x) => interval(x.open, x.close)).sort((a, b) => a[0] - b[0])\n if (!iv.length) continue\n const last = out[out.length - 1]\n if (last && last.to === day - 1 && JSON.stringify(last.iv) === JSON.stringify(iv)) last.to = day\n else out.push({ from: day, to: day, iv })\n }\n return out\n}\n\n/** Hour (24 for an end at midnight) and padded minutes of a minute-of-day value. */\nconst hm = (min: number): [number, string] => [min && min % 1440 === 0 ? 24 : Math.floor(min / 60) % 24, String(min % 60).padStart(2, '0')]\n\n/** Compact text, e.g. `Lun–Vie 9:00–19:00 · Sáb 9:00–14:00` (24 h) or `Mon–Fri 9:00 AM–7:00 PM` (en). */\nexport function formatOpeningHours(hours: OpeningHours, locale = 'es-MX'): string {\n const en = /^en/i.test(locale)\n const name = (d: number): string => {\n const s = new Intl.DateTimeFormat(locale, { weekday: 'short', timeZone: 'UTC' }).format(Date.UTC(2024, 0, d)).replace(/\\.$/, '')\n return s.charAt(0).toUpperCase() + s.slice(1)\n }\n const time = (min: number): string => {\n const [h, m] = hm(min)\n return en ? `${h % 12 || 12}:${m} ${h % 24 < 12 ? 'AM' : 'PM'}` : `${h}:${m}`\n }\n return runs(hours)\n .map((r) => `${name(r.from)}${r.from === r.to ? '' : '–' + name(r.to)} ${r.iv.map(([s, e]) => `${time(s)}–${time(e)}`).join(', ')}`)\n .join(' · ')\n}\n\n/** schema.org `openingHours` strings, one per interval, e.g. `Mo-Fr 09:00-19:00`. */\nexport function toSchemaOpeningHours(hours: OpeningHours): string[] {\n const code = ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su']\n const t = (min: number): string => { const [h, m] = hm(min); return `${String(h).padStart(2, '0')}:${m}` }\n return runs(hours).flatMap((r) =>\n r.iv.map(([s, e]) => `${code[r.from - 1]}${r.from === r.to ? '' : '-' + code[r.to - 1]} ${t(s)}-${t(e)}`),\n )\n}\n"]}
|
package/dist/geo.cjs
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/geo.ts
|
|
4
|
+
var RAD = Math.PI / 180;
|
|
5
|
+
function distanceKm(a, b) {
|
|
6
|
+
const dLat = (b.lat - a.lat) * RAD;
|
|
7
|
+
const dLng = (b.lng - a.lng) * RAD;
|
|
8
|
+
const h = Math.sin(dLat / 2) ** 2 + Math.cos(a.lat * RAD) * Math.cos(b.lat * RAD) * Math.sin(dLng / 2) ** 2;
|
|
9
|
+
return 2 * 6371.0088 * Math.atan2(Math.sqrt(h), Math.sqrt(1 - h));
|
|
10
|
+
}
|
|
11
|
+
function isValidPoint(p) {
|
|
12
|
+
const { lat, lng } = p ?? {};
|
|
13
|
+
return Number.isFinite(lat) && Number.isFinite(lng) && Math.abs(lat) <= 90 && Math.abs(lng) <= 180 && !(lat === 0 && lng === 0);
|
|
14
|
+
}
|
|
15
|
+
function parseLatLng(s) {
|
|
16
|
+
const [a, b, ...rest] = (s ?? "").split(",");
|
|
17
|
+
const p = !rest.length && a?.trim() && b?.trim() ? { lat: Number(a), lng: Number(b) } : null;
|
|
18
|
+
return isValidPoint(p) ? p : null;
|
|
19
|
+
}
|
|
20
|
+
function rankByDistance(items, origin, { radiusKm, limit, getPoint = (i) => i } = {}) {
|
|
21
|
+
const out = [];
|
|
22
|
+
for (const item of items) {
|
|
23
|
+
const p = getPoint(item);
|
|
24
|
+
if (!isValidPoint(p)) continue;
|
|
25
|
+
const d = distanceKm(origin, p);
|
|
26
|
+
if (radiusKm === void 0 || d <= radiusKm) out.push({ item, distanceKm: d });
|
|
27
|
+
}
|
|
28
|
+
out.sort((x, y) => x.distanceKm - y.distanceKm);
|
|
29
|
+
return limit === void 0 ? out : out.slice(0, Math.max(0, limit));
|
|
30
|
+
}
|
|
31
|
+
function formatDistance(km, locale = "es-MX") {
|
|
32
|
+
const m = Math.round(km * 1e3 / 50) * 50;
|
|
33
|
+
const [value, unit, digits] = m < 1e3 ? [m, "meter", 0] : [km, "kilometer", 1];
|
|
34
|
+
return new Intl.NumberFormat(locale, {
|
|
35
|
+
style: "unit",
|
|
36
|
+
unit,
|
|
37
|
+
minimumFractionDigits: digits,
|
|
38
|
+
maximumFractionDigits: digits
|
|
39
|
+
}).format(value);
|
|
40
|
+
}
|
|
41
|
+
function directionsUrl(location, { provider = "google" } = {}) {
|
|
42
|
+
const ll = isValidPoint(location) ? `${location.lat},${location.lng}` : null;
|
|
43
|
+
const text = [location.address, location.city].filter(Boolean).join(", ");
|
|
44
|
+
const dest = ll ?? text;
|
|
45
|
+
if (!dest) return null;
|
|
46
|
+
const q = encodeURIComponent(dest);
|
|
47
|
+
if (provider === "apple") return `https://maps.apple.com/?daddr=${q}`;
|
|
48
|
+
if (provider === "geo") return ll ? `geo:${ll}?q=${ll}` : `geo:0,0?q=${q}`;
|
|
49
|
+
return `https://www.google.com/maps/dir/?api=1&destination=${q}`;
|
|
50
|
+
}
|
|
51
|
+
var toMin = (t) => {
|
|
52
|
+
const m = /^(\d{1,2}):(\d{2})$/.exec(t ?? "");
|
|
53
|
+
return m ? Number(m[1]) * 60 + Number(m[2]) : NaN;
|
|
54
|
+
};
|
|
55
|
+
var interval = (open, close) => {
|
|
56
|
+
const s = toMin(open);
|
|
57
|
+
let e = toMin(close);
|
|
58
|
+
if (Number.isNaN(s) || Number.isNaN(e)) return [];
|
|
59
|
+
if (e <= s) e += 1440;
|
|
60
|
+
return [[s, e]];
|
|
61
|
+
};
|
|
62
|
+
var dayIntervals = (h, day, date) => {
|
|
63
|
+
const x = h.exceptions?.find((e) => e.date === date);
|
|
64
|
+
if (x?.closed) return [];
|
|
65
|
+
if (x && x.open && x.close) return interval(x.open, x.close);
|
|
66
|
+
return h.weekly.filter((w) => w.day === day).flatMap((w) => interval(w.open, w.close));
|
|
67
|
+
};
|
|
68
|
+
function openStatus(hours, timeZone, now = /* @__PURE__ */ new Date()) {
|
|
69
|
+
const fmt = new Intl.DateTimeFormat("en-US", {
|
|
70
|
+
timeZone,
|
|
71
|
+
hourCycle: "h23",
|
|
72
|
+
year: "numeric",
|
|
73
|
+
month: "numeric",
|
|
74
|
+
day: "numeric",
|
|
75
|
+
hour: "numeric",
|
|
76
|
+
minute: "numeric"
|
|
77
|
+
});
|
|
78
|
+
const wall = (ms) => {
|
|
79
|
+
const p = {};
|
|
80
|
+
for (const { type, value } of fmt.formatToParts(ms)) p[type] = Number(value);
|
|
81
|
+
return Date.UTC(p.year, p.month - 1, p.day, (p.hour ?? 0) % 24, p.minute) / 6e4;
|
|
82
|
+
};
|
|
83
|
+
const nowMs = now.getTime();
|
|
84
|
+
const w = wall(nowMs);
|
|
85
|
+
const day0 = Math.floor(w / 1440) * 1440;
|
|
86
|
+
const offset = (ms) => wall(ms) - Math.floor(ms / 6e4);
|
|
87
|
+
const toInstant = (t) => {
|
|
88
|
+
const guess = (t - offset(nowMs)) * 6e4;
|
|
89
|
+
return new Date((t - offset(guess)) * 6e4).toISOString();
|
|
90
|
+
};
|
|
91
|
+
const all = [];
|
|
92
|
+
for (let d = -1; d <= 8; d++) {
|
|
93
|
+
const at = new Date((day0 + d * 1440) * 6e4);
|
|
94
|
+
const day = at.getUTCDay() || 7;
|
|
95
|
+
for (const [s, e] of dayIntervals(hours, day, at.toISOString().slice(0, 10))) all.push([day0 + d * 1440 + s, day0 + d * 1440 + e]);
|
|
96
|
+
}
|
|
97
|
+
all.sort((a, b) => a[0] - b[0]);
|
|
98
|
+
const merged = [];
|
|
99
|
+
for (const iv of all) {
|
|
100
|
+
const last = merged[merged.length - 1];
|
|
101
|
+
if (last && iv[0] <= last[1]) last[1] = Math.max(last[1], iv[1]);
|
|
102
|
+
else merged.push([iv[0], iv[1]]);
|
|
103
|
+
}
|
|
104
|
+
const current = merged.find(([s, e]) => s <= w && w < e);
|
|
105
|
+
if (current) return { open: true, closesAt: toInstant(current[1]), nextOpenAt: null };
|
|
106
|
+
const next = merged.find(([s]) => s > w);
|
|
107
|
+
return { open: false, closesAt: null, nextOpenAt: next ? toInstant(next[0]) : null };
|
|
108
|
+
}
|
|
109
|
+
var runs = (h) => {
|
|
110
|
+
const out = [];
|
|
111
|
+
for (let day = 1; day <= 7; day++) {
|
|
112
|
+
const iv = h.weekly.filter((x) => x.day === day).flatMap((x) => interval(x.open, x.close)).sort((a, b) => a[0] - b[0]);
|
|
113
|
+
if (!iv.length) continue;
|
|
114
|
+
const last = out[out.length - 1];
|
|
115
|
+
if (last && last.to === day - 1 && JSON.stringify(last.iv) === JSON.stringify(iv)) last.to = day;
|
|
116
|
+
else out.push({ from: day, to: day, iv });
|
|
117
|
+
}
|
|
118
|
+
return out;
|
|
119
|
+
};
|
|
120
|
+
var hm = (min) => [min && min % 1440 === 0 ? 24 : Math.floor(min / 60) % 24, String(min % 60).padStart(2, "0")];
|
|
121
|
+
function formatOpeningHours(hours, locale = "es-MX") {
|
|
122
|
+
const en = /^en/i.test(locale);
|
|
123
|
+
const name = (d) => {
|
|
124
|
+
const s = new Intl.DateTimeFormat(locale, { weekday: "short", timeZone: "UTC" }).format(Date.UTC(2024, 0, d)).replace(/\.$/, "");
|
|
125
|
+
return s.charAt(0).toUpperCase() + s.slice(1);
|
|
126
|
+
};
|
|
127
|
+
const time = (min) => {
|
|
128
|
+
const [h, m] = hm(min);
|
|
129
|
+
return en ? `${h % 12 || 12}:${m} ${h % 24 < 12 ? "AM" : "PM"}` : `${h}:${m}`;
|
|
130
|
+
};
|
|
131
|
+
return runs(hours).map((r) => `${name(r.from)}${r.from === r.to ? "" : "\u2013" + name(r.to)} ${r.iv.map(([s, e]) => `${time(s)}\u2013${time(e)}`).join(", ")}`).join(" \xB7 ");
|
|
132
|
+
}
|
|
133
|
+
function toSchemaOpeningHours(hours) {
|
|
134
|
+
const code = ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"];
|
|
135
|
+
const t = (min) => {
|
|
136
|
+
const [h, m] = hm(min);
|
|
137
|
+
return `${String(h).padStart(2, "0")}:${m}`;
|
|
138
|
+
};
|
|
139
|
+
return runs(hours).flatMap(
|
|
140
|
+
(r) => r.iv.map(([s, e]) => `${code[r.from - 1]}${r.from === r.to ? "" : "-" + code[r.to - 1]} ${t(s)}-${t(e)}`)
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
exports.directionsUrl = directionsUrl;
|
|
145
|
+
exports.distanceKm = distanceKm;
|
|
146
|
+
exports.formatDistance = formatDistance;
|
|
147
|
+
exports.formatOpeningHours = formatOpeningHours;
|
|
148
|
+
exports.isValidPoint = isValidPoint;
|
|
149
|
+
exports.openStatus = openStatus;
|
|
150
|
+
exports.parseLatLng = parseLatLng;
|
|
151
|
+
exports.rankByDistance = rankByDistance;
|
|
152
|
+
exports.toSchemaOpeningHours = toSchemaOpeningHours;
|
|
153
|
+
//# sourceMappingURL=geo.cjs.map
|
|
154
|
+
//# sourceMappingURL=geo.cjs.map
|
package/dist/geo.cjs.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/geo.ts"],"names":[],"mappings":"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","file":"geo.cjs","sourcesContent":["/**\n * Geo helpers: pure TypeScript, no network and no DOM. Works in Node 20, edge\n * runtimes and browsers. Behaviour is pinned by `geo-vectors.json`, which the\n * Go side copies verbatim, so keep both implementations in sync.\n */\n\nexport type GeoPoint = { lat: number; lng: number }\n\n/** `(0, 0)` means \"unset\" on this platform, so it is never a valid point. */\ntype MaybePoint = { lat?: number | null; lng?: number | null } | null | undefined\n\nexport type OpeningHours = {\n /** `day`: 1 = Monday ... 7 = Sunday. Several entries per day are allowed. A `close` at or before `open` crosses midnight. */\n weekly: { day: number; open: string; close: string }[]\n /** Dated overrides of the weekly rule. `closed`, or both `open` and `close`. */\n exceptions?: { date: string; closed?: true; open?: string; close?: string; note?: string }[]\n}\n\nexport type OpenStatus = {\n open: boolean\n /** ISO instant (UTC) when the current interval ends; `null` when closed. */\n closesAt: string | null\n /** ISO instant (UTC) of the next opening; `null` when open or none within 8 days. */\n nextOpenAt: string | null\n}\n\nconst RAD = Math.PI / 180\n\n/** Great-circle distance in km (haversine, earth radius 6371.0088 km). */\nexport function distanceKm(a: GeoPoint, b: GeoPoint): number {\n const dLat = (b.lat - a.lat) * RAD\n const dLng = (b.lng - a.lng) * RAD\n const h = Math.sin(dLat / 2) ** 2 + Math.cos(a.lat * RAD) * Math.cos(b.lat * RAD) * Math.sin(dLng / 2) ** 2\n return 2 * 6371.0088 * Math.atan2(Math.sqrt(h), Math.sqrt(1 - h))\n}\n\n/** Finite, in range and not the `(0, 0)` \"unset\" pair. */\nexport function isValidPoint(p: MaybePoint): p is GeoPoint {\n const { lat, lng } = p ?? {}\n // Number.isFinite is false for null, undefined and non-numbers.\n return Number.isFinite(lat) && Number.isFinite(lng) && Math.abs(lat!) <= 90 && Math.abs(lng!) <= 180 && !(lat === 0 && lng === 0)\n}\n\n/** `'19.43,-99.13'` (spaces allowed) to a point; `null` when malformed or invalid. */\nexport function parseLatLng(s: string | null | undefined): GeoPoint | null {\n const [a, b, ...rest] = (s ?? '').split(',')\n const p = !rest.length && a?.trim() && b?.trim() ? { lat: Number(a), lng: Number(b) } : null\n return isValidPoint(p) ? p : null\n}\n\nexport type RankOptions<T> = {\n /** Keep items at most this far away (inclusive). */\n radiusKm?: number\n /** Keep the first N after sorting. */\n limit?: number\n /** Defaults to reading `lat` / `lng` from the item. */\n getPoint?: (item: T) => MaybePoint\n}\n\n/**\n * Sorts by distance from `origin` (stable: ties keep input order). Items without\n * a valid point are EXCLUDED, not placed last, because they have no distance.\n */\nexport function rankByDistance<T>(\n items: readonly T[],\n origin: GeoPoint,\n { radiusKm, limit, getPoint = (i) => i as MaybePoint }: RankOptions<T> = {},\n): { item: T; distanceKm: number }[] {\n const out: { item: T; distanceKm: number }[] = []\n for (const item of items) {\n const p = getPoint(item)\n if (!isValidPoint(p)) continue\n const d = distanceKm(origin, p)\n if (radiusKm === undefined || d <= radiusKm) out.push({ item, distanceKm: d })\n }\n // Array.prototype.sort is stable (ES2019), so ties keep input order.\n out.sort((x, y) => x.distanceKm - y.distanceKm)\n return limit === undefined ? out : out.slice(0, Math.max(0, limit))\n}\n\n/** Under 1 km: meters rounded to 50 m. Otherwise km with one decimal. */\nexport function formatDistance(km: number, locale = 'es-MX'): string {\n const m = Math.round((km * 1000) / 50) * 50\n const [value, unit, digits] = m < 1000 ? [m, 'meter', 0] : [km, 'kilometer', 1]\n return new Intl.NumberFormat(locale, {\n style: 'unit', unit, minimumFractionDigits: digits, maximumFractionDigits: digits,\n }).format(value)\n}\n\n/** Destination for directions: coordinates when valid, else the address text. */\nexport function directionsUrl(\n location: { lat?: number | null; lng?: number | null; address?: string | null; city?: string | null },\n { provider = 'google' }: { provider?: 'google' | 'apple' | 'geo' } = {},\n): string | null {\n const ll = isValidPoint(location) ? `${location.lat},${location.lng}` : null\n const text = [location.address, location.city].filter(Boolean).join(', ')\n const dest = ll ?? text\n if (!dest) return null\n const q = encodeURIComponent(dest)\n if (provider === 'apple') return `https://maps.apple.com/?daddr=${q}`\n if (provider === 'geo') return ll ? `geo:${ll}?q=${ll}` : `geo:0,0?q=${q}`\n return `https://www.google.com/maps/dir/?api=1&destination=${q}`\n}\n\n// --- opening hours -------------------------------------------------------\n\nconst toMin = (t: string | undefined): number => {\n const m = /^(\\d{1,2}):(\\d{2})$/.exec(t ?? '')\n return m ? Number(m[1]) * 60 + Number(m[2]) : NaN\n}\n\ntype Iv = [number, number]\n\n/** Intervals in minutes; an end at or before the start crosses midnight. */\nconst interval = (open: string | undefined, close: string | undefined): Iv[] => {\n const s = toMin(open)\n let e = toMin(close)\n if (Number.isNaN(s) || Number.isNaN(e)) return []\n if (e <= s) e += 1440\n return [[s, e]]\n}\n\nconst dayIntervals = (h: OpeningHours, day: number, date: string): Iv[] => {\n const x = h.exceptions?.find((e) => e.date === date)\n if (x?.closed) return []\n if (x && x.open && x.close) return interval(x.open, x.close)\n return h.weekly.filter((w) => w.day === day).flatMap((w) => interval(w.open, w.close))\n}\n\n/**\n * Whether the place is open at `now` in the IANA `timeZone` (throws RangeError\n * for an unknown zone). Looks 8 days ahead for the next opening. Handles\n * intervals crossing midnight, several per day, dated exceptions and DST.\n */\nexport function openStatus(hours: OpeningHours, timeZone: string, now: Date = new Date()): OpenStatus {\n const fmt = new Intl.DateTimeFormat('en-US', {\n timeZone, hourCycle: 'h23', year: 'numeric', month: 'numeric', day: 'numeric', hour: 'numeric', minute: 'numeric',\n })\n // Wall-clock minutes since the epoch as seen in `timeZone`.\n const wall = (ms: number): number => {\n const p: Record<string, number> = {}\n for (const { type, value } of fmt.formatToParts(ms)) p[type] = Number(value)\n return Date.UTC(p.year!, p.month! - 1, p.day!, (p.hour ?? 0) % 24, p.minute!) / 60000\n }\n const nowMs = now.getTime()\n const w = wall(nowMs)\n const day0 = Math.floor(w / 1440) * 1440\n // Offset from UTC, in minutes, at an instant.\n const offset = (ms: number): number => wall(ms) - Math.floor(ms / 60000)\n const toInstant = (t: number): string => {\n const guess = (t - offset(nowMs)) * 60000\n return new Date((t - offset(guess)) * 60000).toISOString()\n }\n\n const all: Iv[] = []\n for (let d = -1; d <= 8; d++) {\n const at = new Date((day0 + d * 1440) * 60000)\n const day = at.getUTCDay() || 7\n for (const [s, e] of dayIntervals(hours, day, at.toISOString().slice(0, 10))) all.push([day0 + d * 1440 + s, day0 + d * 1440 + e])\n }\n all.sort((a, b) => a[0] - b[0])\n const merged: Iv[] = []\n for (const iv of all) {\n const last = merged[merged.length - 1]\n if (last && iv[0] <= last[1]) last[1] = Math.max(last[1], iv[1])\n else merged.push([iv[0], iv[1]])\n }\n const current = merged.find(([s, e]) => s <= w && w < e)\n if (current) return { open: true, closesAt: toInstant(current[1]), nextOpenAt: null }\n const next = merged.find(([s]) => s > w)\n return { open: false, closesAt: null, nextOpenAt: next ? toInstant(next[0]) : null }\n}\n\ntype Run = { from: number; to: number; iv: Iv[] }\n\n/** Consecutive days (1..7) with identical intervals, in week order. */\nconst runs = (h: OpeningHours): Run[] => {\n const out: Run[] = []\n for (let day = 1; day <= 7; day++) {\n const iv = h.weekly.filter((x) => x.day === day).flatMap((x) => interval(x.open, x.close)).sort((a, b) => a[0] - b[0])\n if (!iv.length) continue\n const last = out[out.length - 1]\n if (last && last.to === day - 1 && JSON.stringify(last.iv) === JSON.stringify(iv)) last.to = day\n else out.push({ from: day, to: day, iv })\n }\n return out\n}\n\n/** Hour (24 for an end at midnight) and padded minutes of a minute-of-day value. */\nconst hm = (min: number): [number, string] => [min && min % 1440 === 0 ? 24 : Math.floor(min / 60) % 24, String(min % 60).padStart(2, '0')]\n\n/** Compact text, e.g. `Lun–Vie 9:00–19:00 · Sáb 9:00–14:00` (24 h) or `Mon–Fri 9:00 AM–7:00 PM` (en). */\nexport function formatOpeningHours(hours: OpeningHours, locale = 'es-MX'): string {\n const en = /^en/i.test(locale)\n const name = (d: number): string => {\n const s = new Intl.DateTimeFormat(locale, { weekday: 'short', timeZone: 'UTC' }).format(Date.UTC(2024, 0, d)).replace(/\\.$/, '')\n return s.charAt(0).toUpperCase() + s.slice(1)\n }\n const time = (min: number): string => {\n const [h, m] = hm(min)\n return en ? `${h % 12 || 12}:${m} ${h % 24 < 12 ? 'AM' : 'PM'}` : `${h}:${m}`\n }\n return runs(hours)\n .map((r) => `${name(r.from)}${r.from === r.to ? '' : '–' + name(r.to)} ${r.iv.map(([s, e]) => `${time(s)}–${time(e)}`).join(', ')}`)\n .join(' · ')\n}\n\n/** schema.org `openingHours` strings, one per interval, e.g. `Mo-Fr 09:00-19:00`. */\nexport function toSchemaOpeningHours(hours: OpeningHours): string[] {\n const code = ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su']\n const t = (min: number): string => { const [h, m] = hm(min); return `${String(h).padStart(2, '0')}:${m}` }\n return runs(hours).flatMap((r) =>\n r.iv.map(([s, e]) => `${code[r.from - 1]}${r.from === r.to ? '' : '-' + code[r.to - 1]} ${t(s)}-${t(e)}`),\n )\n}\n"]}
|
package/dist/geo.d.cts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Geo helpers: pure TypeScript, no network and no DOM. Works in Node 20, edge
|
|
3
|
+
* runtimes and browsers. Behaviour is pinned by `geo-vectors.json`, which the
|
|
4
|
+
* Go side copies verbatim, so keep both implementations in sync.
|
|
5
|
+
*/
|
|
6
|
+
type GeoPoint = {
|
|
7
|
+
lat: number;
|
|
8
|
+
lng: number;
|
|
9
|
+
};
|
|
10
|
+
/** `(0, 0)` means "unset" on this platform, so it is never a valid point. */
|
|
11
|
+
type MaybePoint = {
|
|
12
|
+
lat?: number | null;
|
|
13
|
+
lng?: number | null;
|
|
14
|
+
} | null | undefined;
|
|
15
|
+
type OpeningHours = {
|
|
16
|
+
/** `day`: 1 = Monday ... 7 = Sunday. Several entries per day are allowed. A `close` at or before `open` crosses midnight. */
|
|
17
|
+
weekly: {
|
|
18
|
+
day: number;
|
|
19
|
+
open: string;
|
|
20
|
+
close: string;
|
|
21
|
+
}[];
|
|
22
|
+
/** Dated overrides of the weekly rule. `closed`, or both `open` and `close`. */
|
|
23
|
+
exceptions?: {
|
|
24
|
+
date: string;
|
|
25
|
+
closed?: true;
|
|
26
|
+
open?: string;
|
|
27
|
+
close?: string;
|
|
28
|
+
note?: string;
|
|
29
|
+
}[];
|
|
30
|
+
};
|
|
31
|
+
type OpenStatus = {
|
|
32
|
+
open: boolean;
|
|
33
|
+
/** ISO instant (UTC) when the current interval ends; `null` when closed. */
|
|
34
|
+
closesAt: string | null;
|
|
35
|
+
/** ISO instant (UTC) of the next opening; `null` when open or none within 8 days. */
|
|
36
|
+
nextOpenAt: string | null;
|
|
37
|
+
};
|
|
38
|
+
/** Great-circle distance in km (haversine, earth radius 6371.0088 km). */
|
|
39
|
+
declare function distanceKm(a: GeoPoint, b: GeoPoint): number;
|
|
40
|
+
/** Finite, in range and not the `(0, 0)` "unset" pair. */
|
|
41
|
+
declare function isValidPoint(p: MaybePoint): p is GeoPoint;
|
|
42
|
+
/** `'19.43,-99.13'` (spaces allowed) to a point; `null` when malformed or invalid. */
|
|
43
|
+
declare function parseLatLng(s: string | null | undefined): GeoPoint | null;
|
|
44
|
+
type RankOptions<T> = {
|
|
45
|
+
/** Keep items at most this far away (inclusive). */
|
|
46
|
+
radiusKm?: number;
|
|
47
|
+
/** Keep the first N after sorting. */
|
|
48
|
+
limit?: number;
|
|
49
|
+
/** Defaults to reading `lat` / `lng` from the item. */
|
|
50
|
+
getPoint?: (item: T) => MaybePoint;
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Sorts by distance from `origin` (stable: ties keep input order). Items without
|
|
54
|
+
* a valid point are EXCLUDED, not placed last, because they have no distance.
|
|
55
|
+
*/
|
|
56
|
+
declare function rankByDistance<T>(items: readonly T[], origin: GeoPoint, { radiusKm, limit, getPoint }?: RankOptions<T>): {
|
|
57
|
+
item: T;
|
|
58
|
+
distanceKm: number;
|
|
59
|
+
}[];
|
|
60
|
+
/** Under 1 km: meters rounded to 50 m. Otherwise km with one decimal. */
|
|
61
|
+
declare function formatDistance(km: number, locale?: string): string;
|
|
62
|
+
/** Destination for directions: coordinates when valid, else the address text. */
|
|
63
|
+
declare function directionsUrl(location: {
|
|
64
|
+
lat?: number | null;
|
|
65
|
+
lng?: number | null;
|
|
66
|
+
address?: string | null;
|
|
67
|
+
city?: string | null;
|
|
68
|
+
}, { provider }?: {
|
|
69
|
+
provider?: 'google' | 'apple' | 'geo';
|
|
70
|
+
}): string | null;
|
|
71
|
+
/**
|
|
72
|
+
* Whether the place is open at `now` in the IANA `timeZone` (throws RangeError
|
|
73
|
+
* for an unknown zone). Looks 8 days ahead for the next opening. Handles
|
|
74
|
+
* intervals crossing midnight, several per day, dated exceptions and DST.
|
|
75
|
+
*/
|
|
76
|
+
declare function openStatus(hours: OpeningHours, timeZone: string, now?: Date): OpenStatus;
|
|
77
|
+
/** Compact text, e.g. `Lun–Vie 9:00–19:00 · Sáb 9:00–14:00` (24 h) or `Mon–Fri 9:00 AM–7:00 PM` (en). */
|
|
78
|
+
declare function formatOpeningHours(hours: OpeningHours, locale?: string): string;
|
|
79
|
+
/** schema.org `openingHours` strings, one per interval, e.g. `Mo-Fr 09:00-19:00`. */
|
|
80
|
+
declare function toSchemaOpeningHours(hours: OpeningHours): string[];
|
|
81
|
+
|
|
82
|
+
export { type GeoPoint, type OpenStatus, type OpeningHours, type RankOptions, directionsUrl, distanceKm, formatDistance, formatOpeningHours, isValidPoint, openStatus, parseLatLng, rankByDistance, toSchemaOpeningHours };
|
package/dist/geo.d.ts
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Geo helpers: pure TypeScript, no network and no DOM. Works in Node 20, edge
|
|
3
|
+
* runtimes and browsers. Behaviour is pinned by `geo-vectors.json`, which the
|
|
4
|
+
* Go side copies verbatim, so keep both implementations in sync.
|
|
5
|
+
*/
|
|
6
|
+
type GeoPoint = {
|
|
7
|
+
lat: number;
|
|
8
|
+
lng: number;
|
|
9
|
+
};
|
|
10
|
+
/** `(0, 0)` means "unset" on this platform, so it is never a valid point. */
|
|
11
|
+
type MaybePoint = {
|
|
12
|
+
lat?: number | null;
|
|
13
|
+
lng?: number | null;
|
|
14
|
+
} | null | undefined;
|
|
15
|
+
type OpeningHours = {
|
|
16
|
+
/** `day`: 1 = Monday ... 7 = Sunday. Several entries per day are allowed. A `close` at or before `open` crosses midnight. */
|
|
17
|
+
weekly: {
|
|
18
|
+
day: number;
|
|
19
|
+
open: string;
|
|
20
|
+
close: string;
|
|
21
|
+
}[];
|
|
22
|
+
/** Dated overrides of the weekly rule. `closed`, or both `open` and `close`. */
|
|
23
|
+
exceptions?: {
|
|
24
|
+
date: string;
|
|
25
|
+
closed?: true;
|
|
26
|
+
open?: string;
|
|
27
|
+
close?: string;
|
|
28
|
+
note?: string;
|
|
29
|
+
}[];
|
|
30
|
+
};
|
|
31
|
+
type OpenStatus = {
|
|
32
|
+
open: boolean;
|
|
33
|
+
/** ISO instant (UTC) when the current interval ends; `null` when closed. */
|
|
34
|
+
closesAt: string | null;
|
|
35
|
+
/** ISO instant (UTC) of the next opening; `null` when open or none within 8 days. */
|
|
36
|
+
nextOpenAt: string | null;
|
|
37
|
+
};
|
|
38
|
+
/** Great-circle distance in km (haversine, earth radius 6371.0088 km). */
|
|
39
|
+
declare function distanceKm(a: GeoPoint, b: GeoPoint): number;
|
|
40
|
+
/** Finite, in range and not the `(0, 0)` "unset" pair. */
|
|
41
|
+
declare function isValidPoint(p: MaybePoint): p is GeoPoint;
|
|
42
|
+
/** `'19.43,-99.13'` (spaces allowed) to a point; `null` when malformed or invalid. */
|
|
43
|
+
declare function parseLatLng(s: string | null | undefined): GeoPoint | null;
|
|
44
|
+
type RankOptions<T> = {
|
|
45
|
+
/** Keep items at most this far away (inclusive). */
|
|
46
|
+
radiusKm?: number;
|
|
47
|
+
/** Keep the first N after sorting. */
|
|
48
|
+
limit?: number;
|
|
49
|
+
/** Defaults to reading `lat` / `lng` from the item. */
|
|
50
|
+
getPoint?: (item: T) => MaybePoint;
|
|
51
|
+
};
|
|
52
|
+
/**
|
|
53
|
+
* Sorts by distance from `origin` (stable: ties keep input order). Items without
|
|
54
|
+
* a valid point are EXCLUDED, not placed last, because they have no distance.
|
|
55
|
+
*/
|
|
56
|
+
declare function rankByDistance<T>(items: readonly T[], origin: GeoPoint, { radiusKm, limit, getPoint }?: RankOptions<T>): {
|
|
57
|
+
item: T;
|
|
58
|
+
distanceKm: number;
|
|
59
|
+
}[];
|
|
60
|
+
/** Under 1 km: meters rounded to 50 m. Otherwise km with one decimal. */
|
|
61
|
+
declare function formatDistance(km: number, locale?: string): string;
|
|
62
|
+
/** Destination for directions: coordinates when valid, else the address text. */
|
|
63
|
+
declare function directionsUrl(location: {
|
|
64
|
+
lat?: number | null;
|
|
65
|
+
lng?: number | null;
|
|
66
|
+
address?: string | null;
|
|
67
|
+
city?: string | null;
|
|
68
|
+
}, { provider }?: {
|
|
69
|
+
provider?: 'google' | 'apple' | 'geo';
|
|
70
|
+
}): string | null;
|
|
71
|
+
/**
|
|
72
|
+
* Whether the place is open at `now` in the IANA `timeZone` (throws RangeError
|
|
73
|
+
* for an unknown zone). Looks 8 days ahead for the next opening. Handles
|
|
74
|
+
* intervals crossing midnight, several per day, dated exceptions and DST.
|
|
75
|
+
*/
|
|
76
|
+
declare function openStatus(hours: OpeningHours, timeZone: string, now?: Date): OpenStatus;
|
|
77
|
+
/** Compact text, e.g. `Lun–Vie 9:00–19:00 · Sáb 9:00–14:00` (24 h) or `Mon–Fri 9:00 AM–7:00 PM` (en). */
|
|
78
|
+
declare function formatOpeningHours(hours: OpeningHours, locale?: string): string;
|
|
79
|
+
/** schema.org `openingHours` strings, one per interval, e.g. `Mo-Fr 09:00-19:00`. */
|
|
80
|
+
declare function toSchemaOpeningHours(hours: OpeningHours): string[];
|
|
81
|
+
|
|
82
|
+
export { type GeoPoint, type OpenStatus, type OpeningHours, type RankOptions, directionsUrl, distanceKm, formatDistance, formatOpeningHours, isValidPoint, openStatus, parseLatLng, rankByDistance, toSchemaOpeningHours };
|
package/dist/geo.js
ADDED
package/dist/geo.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"names":[],"mappings":"","file":"geo.js"}
|
package/dist/index.cjs
CHANGED
|
@@ -33,6 +33,10 @@ var STATUS_CODES = {
|
|
|
33
33
|
function defaultCodeForStatus(status) {
|
|
34
34
|
return STATUS_CODES[status] ?? (status >= 500 ? "server_error" : "invalid_response");
|
|
35
35
|
}
|
|
36
|
+
function isValidPoint(p) {
|
|
37
|
+
const { lat, lng } = p ?? {};
|
|
38
|
+
return Number.isFinite(lat) && Number.isFinite(lng) && Math.abs(lat) <= 90 && Math.abs(lng) <= 180 && !(lat === 0 && lng === 0);
|
|
39
|
+
}
|
|
36
40
|
|
|
37
41
|
// src/money.ts
|
|
38
42
|
function parseMoney(money) {
|
|
@@ -254,6 +258,22 @@ function createStorefrontClient(config) {
|
|
|
254
258
|
/** `null` when the collection does not exist. */
|
|
255
259
|
get: (handle, options) => getOrNull(`/storefront/collections/${enc(handle)}`, void 0, options)
|
|
256
260
|
},
|
|
261
|
+
locations: {
|
|
262
|
+
/** Branches of the shop. With `postal_code` they come ordered by distance (`distance_km`). */
|
|
263
|
+
list: (params, options) => get("/storefront/locations", params, options),
|
|
264
|
+
/** `null` when the branch does not exist or is not visible. */
|
|
265
|
+
get: (id, options) => getOrNull(`/storefront/locations/${enc(id)}`, void 0, options),
|
|
266
|
+
/**
|
|
267
|
+
* Branches ordered by distance to a point, closest first. The server rounds the coordinates (0.01°, ~1 km)
|
|
268
|
+
* and never stores them; this client does not log or keep them either. The answer is `private, no-store`
|
|
269
|
+
* (it depends on the buyer's position), so do not put it in a shared cache. Note `onRequest` hooks and
|
|
270
|
+
* `fetch` wrappers do see the URL: do not log it. Throws `TypeError` for an invalid point.
|
|
271
|
+
*/
|
|
272
|
+
nearest: (point, options) => {
|
|
273
|
+
if (!isValidPoint(point)) throw new TypeError("locations.nearest: lat/lng must be a valid point");
|
|
274
|
+
return get("/storefront/locations/nearest", { lat: point.lat, lng: point.lng }, { cache: "no-store", ...options });
|
|
275
|
+
}
|
|
276
|
+
},
|
|
257
277
|
stock: {
|
|
258
278
|
/** Stock for up to 250 SKUs. */
|
|
259
279
|
get: (skus, options) => get("/storefront/stock", { sku: skus }, options)
|