tz-at-point 1.0.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/LICENSE +21 -0
- package/README.md +337 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +396 -0
- package/dist/geo.d.ts +7 -0
- package/dist/geo.js +19 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +33 -0
- package/dist/key.d.ts +15 -0
- package/dist/key.js +28 -0
- package/dist/lookup.d.ts +55 -0
- package/dist/lookup.js +103 -0
- package/dist/points.d.ts +8 -0
- package/dist/points.js +62 -0
- package/dist/radius.d.ts +12 -0
- package/dist/radius.js +27 -0
- package/dist/table.d.ts +38 -0
- package/dist/table.js +76 -0
- package/dist/text.d.ts +7 -0
- package/dist/text.js +16 -0
- package/llms.txt +18 -0
- package/package.json +90 -0
- package/skills/tz-at-point/SKILL.md +106 -0
- package/skills/tz-at-point/references/api.md +115 -0
- package/skills/tz-at-point/references/cli.md +108 -0
- package/skills/tz-at-point/references/setup.md +107 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# tz-at-point API
|
|
2
|
+
|
|
3
|
+
```ts
|
|
4
|
+
import { createLookup, pointKey, type Lookup, type Options, type Result, type Source, type Table } from 'tz-at-point';
|
|
5
|
+
```
|
|
6
|
+
|
|
7
|
+
Node 20.19+ or 22.12+. Published types work with TypeScript 5.0 or later.
|
|
8
|
+
|
|
9
|
+
Two entry points, same function:
|
|
10
|
+
|
|
11
|
+
- `tz-at-point` answers points outside the table with a bundled raster (74KB in a bundle; 77KB with this package around it).
|
|
12
|
+
- `tz-at-point/core` has no fallback, so the raster never enters your bundle (~3KB). Points outside the table return `{ zone: null, source: null }` unless you pass your own `fallback`.
|
|
13
|
+
|
|
14
|
+
## `createLookup(table, options?)`
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
function createLookup(table: unknown, options?: Options): Lookup;
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Builds a lookup from a table written by `tz-at-point build`. Call it once per process, at module scope: it validates the table and builds a grid index. Startup and lookup timings are in the README under "What it promises"; `scripts/bench.mjs` in the repo measures them on your machine.
|
|
21
|
+
|
|
22
|
+
- `table` is typed `unknown` so a JSON import passes without a cast. It is validated at runtime.
|
|
23
|
+
- Throws a `TypeError` if the table is malformed. The message starts with `tz-at-point table:` and names the first bad entry.
|
|
24
|
+
- The returned `Lookup` never throws and reads no files.
|
|
25
|
+
|
|
26
|
+
Resolution order for a lookup:
|
|
27
|
+
|
|
28
|
+
1. **Exact key.** The coordinate rounded to 4 decimals is in the table → `{ zone, source: 'table' }`. A key covers a cell about 11m across, including a radius-0 entry's cell, even if part of that cell is across a border.
|
|
29
|
+
2. **Nearest covering entry.** The nearest entry whose own safe radius contains the point → `source: 'table-near'`.
|
|
30
|
+
3. **Fallback.** `options.fallback(lat, lng)` → `source: 'raster'`, if it returns a valid zone name.
|
|
31
|
+
4. Otherwise `{ zone: null, source: null }`.
|
|
32
|
+
|
|
33
|
+
## `Options`
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
interface Options {
|
|
37
|
+
fallback?: ((lat: number, lng: number) => string | null | undefined) | null;
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
- `fallback` defaults to `@photostructure/tz-lookup`: a 74KB raster once bundled, no file reads, approximate near borders.
|
|
42
|
+
- `fallback: null` answers only from the table; everything outside it returns `{ zone: null, source: null }`.
|
|
43
|
+
- A fallback that throws, returns a non-string, or returns something that is not a zone name is treated as no answer.
|
|
44
|
+
|
|
45
|
+
## `Lookup`
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
type Lookup = (lat: unknown, lng: unknown) => Result;
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Accepts anything. `lat` and `lng` must be finite numbers with `|lat| <= 90` and `|lng| <= 180`; strings such as `'51.5'` are not coordinates and give `{ zone: null, source: null }`. Parse numbers before calling.
|
|
52
|
+
|
|
53
|
+
## `Result` and `Source`
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
type Source = 'table' | 'table-near' | 'raster';
|
|
57
|
+
type Result = { zone: string; source: Source } | { zone: null; source: null };
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
`zone` is an IANA name such as `Europe/London` or `America/Argentina/Buenos_Aires`. Use it with `Intl.DateTimeFormat(locale, { timeZone: zone, ... })` to format local times; the UTC offset on a given date comes from the runtime's timezone data.
|
|
61
|
+
|
|
62
|
+
## `pointKey(lat, lng)`
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
function pointKey(lat: unknown, lng: unknown): string | null;
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
The table key for a coordinate: `pointKey(51.55614, -0.27936)` is `'51.5561,-0.2794'`. Returns `null` when the input is not a coordinate. Longitude -180 is written as 180. Useful for checking whether a point is in a table:
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
const key = pointKey(lat, lng);
|
|
72
|
+
const known = key !== null && Object.hasOwn(table.points, key);
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Accuracy
|
|
76
|
+
|
|
77
|
+
A `table` or `table-near` answer matches the polygons, with two exceptions:
|
|
78
|
+
|
|
79
|
+
- Inside a key's ~11m cell, the key's zone is the answer, border or not.
|
|
80
|
+
- A piece of another zone smaller than the probe lattice can resolve (about 7m) can hide inside the radius. Everything larger is found, including near the radius edge, because the radius keeps a verified ring beyond it.
|
|
81
|
+
|
|
82
|
+
`raster` answers are approximate everywhere, and wrong near borders often enough to matter (see the README).
|
|
83
|
+
|
|
84
|
+
## `Table`
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
interface Table {
|
|
88
|
+
v: 1;
|
|
89
|
+
attribution?: string;
|
|
90
|
+
maxRadius?: number;
|
|
91
|
+
geoTz?: string;
|
|
92
|
+
points: Record<string, [zone: string, radius: number]>;
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
```json
|
|
97
|
+
{
|
|
98
|
+
"v": 1,
|
|
99
|
+
"attribution": "Timezone boundaries from OpenStreetMap (https://www.openstreetmap.org/copyright), ODbL 1.0. Zone names from the IANA tz database, public domain.",
|
|
100
|
+
"geoTz": "8.1.9",
|
|
101
|
+
"maxRadius": 250,
|
|
102
|
+
"points": {
|
|
103
|
+
"51.4394,4.9275": ["Europe/Amsterdam",0],
|
|
104
|
+
"51.5561,-0.2794": ["Europe/London",250]
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
- Keys: `lat,lng`, each with exactly 4 decimals, as `pointKey` produces them.
|
|
110
|
+
- Zone: an IANA name, letters, digits, `_`, `+`, `-`, up to three `/`-separated parts.
|
|
111
|
+
- Radius: metres, a multiple of 10 from 0 to 1000. It is the widest disc around the key that build's probes vouch for: a ~10m lattice, with a whole verified ring beyond the radius itself, because a ring only samples its circle at intervals. Radius 0 means another zone is within 10m, unless the table was built with `--max-radius 0`.
|
|
112
|
+
- `maxRadius`: the `--max-radius` the table was built with. A later `build` with a different value re-resolves every entry, because a radius means nothing without the cap it was probed under.
|
|
113
|
+
- `geoTz`: the geo-tz version whose boundaries produced these zones. `check` prints it, so a boundary data bump is visible in a diff instead of silent.
|
|
114
|
+
- The zone and radius are for the rounded key, not for the original input coordinate.
|
|
115
|
+
- `tz-at-point build` writes keys sorted, one entry per line. Don't edit the file by hand.
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# tz-at-point CLI
|
|
2
|
+
|
|
3
|
+
```
|
|
4
|
+
usage:
|
|
5
|
+
tz-at-point build <points.json|points.csv> -o <zones.json> [--check | --refresh] [--max-radius 250]
|
|
6
|
+
tz-at-point check <zones.json> [--raster]
|
|
7
|
+
tz-at-point --version
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
`build` and `check` need geo-tz installed as a dev dependency (8.0.0 or later, for its `geo-tz/all` export). `build --check`, and a `build` with nothing to resolve, never load it. `--help` / `-h` prints the usage and exits 0, before or after a command. `--version` prints the package version alone, exit 0.
|
|
11
|
+
|
|
12
|
+
## `tz-at-point build <points> -o <zones.json>`
|
|
13
|
+
|
|
14
|
+
Adds every point missing from the table and never removes an entry. It never re-validates entries already in the table; `check` does that. For each new key it resolves the zone with `geo-tz/all` (not the default geo-tz export, whose merged dataset names some zones differently) and probes the safe radius.
|
|
15
|
+
|
|
16
|
+
**Points file** (chosen by extension, case-insensitive):
|
|
17
|
+
|
|
18
|
+
- `.json`: an array of `[lat, lng]` pairs or `{ "lat": …, "lng": … }` objects. Extra fields are ignored.
|
|
19
|
+
- `.csv`: a header row with `lat` and `lng` columns in any order; other columns are ignored. If a header name repeats, the first column with that name is used. Fields may be double-quoted, with `""` for a quote. Blank lines are skipped. Quoted line breaks are not supported. Numbers must be plain decimals (`51.5561`, `-0.2794`).
|
|
20
|
+
- A byte-order mark is ignored in both.
|
|
21
|
+
|
|
22
|
+
**Flags**
|
|
23
|
+
|
|
24
|
+
| Flag | Meaning |
|
|
25
|
+
|---|---|
|
|
26
|
+
| `-o, --out <file>` | The table to create or extend. Required. If it is a symlink, the file it points to is updated and keeps its permissions. |
|
|
27
|
+
| `--check` | Change nothing. Exit 0 if every point is in the table, 1 if any is missing or the table doesn't exist. Doesn't need geo-tz. |
|
|
28
|
+
| `--refresh` | Re-resolve every entry, old and new, with the installed geo-tz and the given `--max-radius`, and report how many changed (a radius change counts). Pass the same `--max-radius` you built with, or every wider radius shrinks to the default. Can't be combined with `--check`. |
|
|
29
|
+
| `--max-radius <m>` | Largest safe radius to probe. A multiple of 10 from 0 to 1000; default 250. Cost grows with its square: 2,218 probes per point at 250, 8,357 at 500. The table records the value, and a later build with a different one re-resolves every entry. |
|
|
30
|
+
| `-h, --help` | Print usage. |
|
|
31
|
+
|
|
32
|
+
Writes are atomic: a temp file is written and synced, then renamed over the table, so an interrupted build leaves the old table in place. Parallel builds of the same table are safe: each merges with what is on disk and verifies its own entries survived. It also runs under Node's permission model, where fsync and fchmod are unavailable (durability and mode preservation are skipped). A hard link to the table is not followed: the rename leaves the other link on the old contents.
|
|
33
|
+
|
|
34
|
+
Paths and table content in messages are escaped to printable ASCII. Only the usage text keeps its line breaks; every other message is one line.
|
|
35
|
+
|
|
36
|
+
**Output**
|
|
37
|
+
|
|
38
|
+
- `wrote zones.json: 5 points (5 resolved)`: table written. With `--refresh`: `(5 resolved, 1 changed)`.
|
|
39
|
+
- `re-resolved at --max-radius 250: wrote zones.json: …`: the table was built with a different `--max-radius`, so every entry was probed again under the new one.
|
|
40
|
+
- `zones.json kept changing underneath this build; run it again`: five merge attempts in a row were overtaken by other builds writing the same table. Rare; rerun.
|
|
41
|
+
- `up to date: 5 points`: nothing to add; the file was not touched. With `--check`, exit 0.
|
|
42
|
+
- `1 point missing from zones.json:` followed by the missing keys and `run: npx tz-at-point build points.csv -o zones.json`: from `--check`, exit 1. Run that command and commit the table. The `run:` line repeats a non-default `--max-radius`.
|
|
43
|
+
- `zones.json does not exist yet`: from `--check` when there is no table, exit 1, followed by the same key list and `run:` line.
|
|
44
|
+
- Counts (`5 points`) are entries in the table, not lines in the points file: several coordinates can round to one key.
|
|
45
|
+
- `warning: 51.4394,4.9275 is within 10m of another zone; lookups that round to it answer Europe/Amsterdam, even from across the border`: the entry has radius 0. Its whole key cell (about 11m) answers one zone. Printed on stderr, once per key, on every build whose points include one. Not an error, and not printed with `--check` or when you passed `--max-radius 0`.
|
|
46
|
+
- `warning: 51.449039,4.930128 is in Europe/Brussels, but its key 51.4490,4.9301 is in Europe/Amsterdam; lookups there answer Europe/Amsterdam`: the input point is within a few metres of a border, across it from its rounded key. On stderr. Only points whose key is added in that run are compared, so a new point that shares an existing key is never checked. Move the coordinate onto the correct side if it matters.
|
|
47
|
+
- `warning: zones.json still records geo-tz 8.1.9; new points were resolved with 8.2.0; run --refresh to re-resolve the rest`: the installed geo-tz is newer than the one the table records, and this build only resolved the points that were missing. The table keeps the older version, because most of its entries were produced by it. `--refresh` re-resolves everything and records the new one. On stderr, exit 0.
|
|
48
|
+
- `warning: points.csv has no points, so zones.json answers nothing`: the points file parsed but held no rows (a header alone, or `[]`) and the table has no entries either. The table is still written, empty, so an import of it works from the first commit; but every lookup falls through to the raster until points are added. Not printed when the table already has entries, or with `--check`. On stderr, exit 0.
|
|
49
|
+
- `...and 12 more`: lists are cut at 20 lines.
|
|
50
|
+
|
|
51
|
+
## `tz-at-point check <zones.json> [--raster]`
|
|
52
|
+
|
|
53
|
+
Re-runs build's probes for every entry against the installed geo-tz, and checks every zone name against the runtime's `Intl`. Run it after upgrading geo-tz or Node, or in CI (give it a timeout on tables you didn't build: cost grows with each radius squared).
|
|
54
|
+
|
|
55
|
+
It uses the same lattice build used, so it finds stale entries, hand edits and moved boundaries. It cannot find something build's probes were too coarse to see in the first place.
|
|
56
|
+
|
|
57
|
+
**Output**
|
|
58
|
+
|
|
59
|
+
- With `--raster`: `raster: 1 of 2 points disagrees with the table, 1 by a different UTC offset`, followed by each disagreement (`65.8481,24.1466: raster says Europe/Stockholm, table says Europe/Helsinki`). This is what the bundled fallback alone would answer for your own points, so it shows what the table is buying you. Informational; it never changes the exit code. If the count is 0, the raster alone would do for your data.
|
|
60
|
+
- `built with geo-tz 8.1.9`, or `built with geo-tz 8.1.9, checked against 8.2.0` when the installed version has moved on. Informational: `check` re-probes against what is installed either way, so a version difference alone is not a failure.
|
|
61
|
+
- `ok: 5 points match the polygons` (`1 point matches`): exit 0.
|
|
62
|
+
- `FAIL 51.4926,7.4519: table says Europe/Paris, polygons say Europe/Berlin`: the entry's zone is wrong for the installed geo-tz: the table was edited, merged badly, or boundaries changed. Exit 1.
|
|
63
|
+
- `FAIL 51.4394,4.9275: radius 500m reaches another zone`: a probe inside the stored radius found another zone. Exit 1.
|
|
64
|
+
- `FAIL America/Ciudad_Juarez: not a zone this runtime's Intl accepts`: the Node/ICU running `check` doesn't know that zone. Exit 1.
|
|
65
|
+
- `fix: update Node; its timezone data doesn't know these zones`: printed when any zone failed the `Intl` check. Upgrade Node; rebuilding won't change the zone name.
|
|
66
|
+
- `fix: rebuild the table with --refresh`: printed when any entry's zone or radius failed. Run `tz-at-point build <points> -o <zones.json> --refresh` (`check` only knows the table path, so supply the points file), then `check` again, and review `git diff` before committing.
|
|
67
|
+
|
|
68
|
+
## Errors (exit 2)
|
|
69
|
+
|
|
70
|
+
Every error line starts with `tz-at-point: ` and exits 2.
|
|
71
|
+
|
|
72
|
+
| Message | Cause |
|
|
73
|
+
|---|---|
|
|
74
|
+
| the usage text | Missing or extra arguments, or an unknown command |
|
|
75
|
+
| `unknown option --x` followed by the usage text | A flag that doesn't exist |
|
|
76
|
+
| `--max-radius needs a value` | An option given no value, or a value starting with `-` (write `--max-radius=…` for those) |
|
|
77
|
+
| `--check does not take a value` | A boolean flag written as `--check=yes` |
|
|
78
|
+
| `this command needs geo-tz 8 or later: npm install --save-dev geo-tz` | geo-tz missing or too old |
|
|
79
|
+
| `zones.json: not valid JSON` | The file isn't JSON. Its content is never echoed. |
|
|
80
|
+
| `points.txt: points must be a .json or .csv file` | Wrong extension |
|
|
81
|
+
| `--check and --refresh cannot be combined` | Both flags given |
|
|
82
|
+
| `--max-radius must be a multiple of 10 from 0 to 1000` | Bad `--max-radius` |
|
|
83
|
+
| `no zone found for KEY` | geo-tz returned nothing for a coordinate |
|
|
84
|
+
| `KEY: geo-tz returned an invalid zone name` | The installed geo-tz (or a dataset `GEO_TZ_DATA_PATH` pointed it at) answered with something that is not a zone name; nothing was written |
|
|
85
|
+
| `JSON points must be an array` | A JSON points file that isn't an array |
|
|
86
|
+
| `row 3: expected [lat, lng] or { lat, lng } with numbers in range` | A bad JSON row (strings, out of range, wrong length) |
|
|
87
|
+
| `CSV header must have lat and lng columns` | Header missing `lat` or `lng` (other names like `latitude` aren't recognised) |
|
|
88
|
+
| `line 4: unterminated quote (quoted line breaks are not supported)` | A CSV quote left open |
|
|
89
|
+
| `line 4: lat and lng must be decimal numbers in range` | Empty, hex, exponent or out-of-range values |
|
|
90
|
+
| `zones.json: entry "KEY" is not a canonical point key` | Table key not in `pointKey` form (hand edit) |
|
|
91
|
+
| `zones.json: entry "KEY" must map to [zone, radius]` | Wrong value shape |
|
|
92
|
+
| `zones.json: entry "KEY" has an invalid zone name` | Zone isn't an IANA-style name |
|
|
93
|
+
| `zones.json: entry "KEY" has radius 255; expected a multiple of 10 from 0 to 1000` | Bad radius |
|
|
94
|
+
| `zones.json: unsupported version 2` | Not a v1 table |
|
|
95
|
+
| `zones.json: attribution must be a string` | The optional `attribution` field was hand-edited into something other than text |
|
|
96
|
+
| `zones.json: must be a plain object` / `zones.json: points must be a plain object` | Not a table at all |
|
|
97
|
+
| `points.json: no such file or directory` / `is a directory` / `not a directory` / `permission denied` / `symlink loop` / `name too long` | The points file or table can't be read, or the output path can't be examined; the path is the one you passed |
|
|
98
|
+
| `/abs/dir: no such directory` / `not writable` | The directory the table would be written into doesn't exist or isn't writable; checked before any point is resolved. This one names the directory as resolved: absolute, and with a symlinked output followed to where it points |
|
|
99
|
+
|
|
100
|
+
Paths and table content in messages are escaped to printable ASCII.
|
|
101
|
+
|
|
102
|
+
## Exit codes
|
|
103
|
+
|
|
104
|
+
| Code | Meaning |
|
|
105
|
+
|---|---|
|
|
106
|
+
| 0 | Success, or `--help` |
|
|
107
|
+
| 1 | `build --check` found missing points or no table; `check` found a failure |
|
|
108
|
+
| 2 | Bad arguments or input |
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# Setting up tz-at-point in a project
|
|
2
|
+
|
|
3
|
+
## Install
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npm install tz-at-point
|
|
7
|
+
npm install --save-dev geo-tz
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
geo-tz (about 74MB, 29 packages) is only for `build` and `check`, and nothing at runtime imports it. At runtime a consumer installs two packages: this one and the raster.
|
|
11
|
+
|
|
12
|
+
Generated tables carry an `attribution` line naming OpenStreetMap and the ODbL, because the boundaries come from there. Anything you ship that bundles the raster should credit OpenStreetMap; see the README's data sources section. tz-at-point needs Node 20.19+ or 22.12+, and is ESM: `require('tz-at-point')` works on 20.19+ and 22.12+, the same floors.
|
|
13
|
+
|
|
14
|
+
Importing from `tz-at-point/core` gives the same `createLookup` with no raster fallback, so the raster (74KB bundled) never enters your bundle. Points outside the table then answer `{ zone: null }`.
|
|
15
|
+
|
|
16
|
+
## The points file
|
|
17
|
+
|
|
18
|
+
`tz-at-point build` reads `.csv` (header with `lat` and `lng` columns) or `.json` (array of `[lat, lng]` or `{ lat, lng }`) directly. If the project already has one of those, pass it as is.
|
|
19
|
+
|
|
20
|
+
If the data uses other names or lives elsewhere, write a points file once with a short script, and rerun it when the data changes:
|
|
21
|
+
|
|
22
|
+
```js
|
|
23
|
+
// scripts/points.mjs: node scripts/points.mjs > data/points.json
|
|
24
|
+
import stores from '../data/stores.json' with { type: 'json' };
|
|
25
|
+
console.log(JSON.stringify(stores.map((s) => ({ lat: Number(s.latitude), lng: Number(s.longitude) }))));
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Keep the points file and `zones.json` in the repository, next to the code that imports the table.
|
|
29
|
+
|
|
30
|
+
## Importing the table
|
|
31
|
+
|
|
32
|
+
The table must be imported, not read with `fs`, so the bundler embeds it.
|
|
33
|
+
|
|
34
|
+
| Setup | Import |
|
|
35
|
+
|---|---|
|
|
36
|
+
| Node ESM, TypeScript `module: nodenext` | `import table from './zones.json' with { type: 'json' };` Node requires the attribute; TypeScript accepts it from 5.3 and enforces it from 5.7. `module: node16` rejects it. |
|
|
37
|
+
| Bundlers: Next.js, Vite, esbuild, webpack, TypeScript `moduleResolution: bundler` | `import table from './zones.json';` also works |
|
|
38
|
+
| CommonJS | `const table = require('./zones.json');` works, but `require('tz-at-point')` works on the same Node floors, 20.19+ and 22.12+ |
|
|
39
|
+
|
|
40
|
+
- Add `"resolveJsonModule": true` to `tsconfig.json`. Required on every TypeScript version tested (5.0 through 7.0) when `moduleResolution` is `nodenext`, otherwise the import fails with `TS2732`; harmless in bundler setups.
|
|
41
|
+
- Node prints no warning for JSON imports from 22.12 on.
|
|
42
|
+
- Import other data the function needs (venue lists) the same way. A `readFileSync` of a data file in a serverless handler fails for the same reason geo-tz does. If that data is a CSV, keep it as JSON instead and pass the JSON to `tz-at-point build`, or generate the JSON from the CSV with a script and commit both.
|
|
43
|
+
- If a bundler rejects the `with { type: 'json' }` syntax, use the plain `import table from './zones.json'` form.
|
|
44
|
+
|
|
45
|
+
```js
|
|
46
|
+
import { createLookup } from 'tz-at-point';
|
|
47
|
+
import table from '../data/zones.json' with { type: 'json' };
|
|
48
|
+
|
|
49
|
+
export const zoneAt = createLookup(table);
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Formatting local time
|
|
53
|
+
|
|
54
|
+
Include the date: a kickoff at 21:30 UTC is 00:30 the next day in Helsinki.
|
|
55
|
+
|
|
56
|
+
```js
|
|
57
|
+
const format = (zone, utcIso) => new Intl.DateTimeFormat('en-GB', {
|
|
58
|
+
timeZone: zone, weekday: 'short', day: 'numeric', month: 'short', hour: '2-digit', minute: '2-digit', hourCycle: 'h23',
|
|
59
|
+
}).format(new Date(utcIso));
|
|
60
|
+
// format('Europe/Helsinki', '2026-09-20T21:30:00Z') → 'Mon 21 Sept, 00:30'
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## A test worth adding
|
|
64
|
+
|
|
65
|
+
A point from the points file answering `raster` means the committed table is stale. Tests run in Node, not in the function, so they may read files; this one imports the JSON the handler uses. With a CSV points file and no JSON list, `build --check` in CI already covers it.
|
|
66
|
+
|
|
67
|
+
```js
|
|
68
|
+
import { test } from 'node:test';
|
|
69
|
+
import assert from 'node:assert/strict';
|
|
70
|
+
import venues from '../data/venues.json' with { type: 'json' };
|
|
71
|
+
import { zoneAt } from '../src/zones.js';
|
|
72
|
+
|
|
73
|
+
test('every venue resolves from the committed table', () => {
|
|
74
|
+
// Not notEqual(source, 'raster'): that also passes for { zone: null }, which is what a bad coordinate gives.
|
|
75
|
+
for (const v of venues) assert.ok(['table', 'table-near'].includes(zoneAt(v.lat, v.lng).source), v.name);
|
|
76
|
+
});
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## CI
|
|
80
|
+
|
|
81
|
+
```yaml
|
|
82
|
+
# .github/workflows/ci.yml, after `npm ci`
|
|
83
|
+
- name: Timezone table covers every point
|
|
84
|
+
run: npx tz-at-point build data/points.csv -o data/zones.json --check
|
|
85
|
+
- name: Timezone table matches current boundaries
|
|
86
|
+
run: npx tz-at-point check data/zones.json
|
|
87
|
+
timeout-minutes: 10
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
The first step is fast and doesn't load geo-tz. The second re-probes every entry; on large tables, run it on dependency-update pull requests or on a schedule instead of every push.
|
|
91
|
+
|
|
92
|
+
## Upgrading geo-tz, tz-at-point or Node, or when `check` fails
|
|
93
|
+
|
|
94
|
+
Treat any change to the installed geo-tz version as a boundary-data change, including one from `npm update` or a Dependabot pull request. A Node upgrade can change which zone names `Intl` accepts.
|
|
95
|
+
|
|
96
|
+
```sh
|
|
97
|
+
npm install --save-dev geo-tz@latest
|
|
98
|
+
npx tz-at-point build data/points.csv -o data/zones.json --refresh # add the same --max-radius you built with
|
|
99
|
+
npx tz-at-point check data/zones.json
|
|
100
|
+
git diff data/zones.json
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Review changed entries before committing: a changed zone is a changed local time.
|
|
104
|
+
|
|
105
|
+
## Removing points
|
|
106
|
+
|
|
107
|
+
`build` never deletes entries, so a point removed from the data stays in the table (harmless). To prune, delete `zones.json` and run `build` again; that re-resolves every point.
|