File size: 2,830 Bytes
fa9c65f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
/**
 * Shared scalar guards for the analysis adapters (#5696).
 *
 * Both adapter modules map untrusted seeded-cache JSON onto the analysis
 * cores' input types, so both need the same defensive primitives. They were
 * written independently and grew two names for each concept
 * (`finiteNumber`/`num`, `nonEmptyString`/`str`); one definition removes the
 * drift risk, since a divergence in null-handling here silently empties a
 * result set rather than raising.
 *
 * Semantics are deliberately permissive on INPUT (a seeder that stores a
 * number as a string still parses) and strict on VALIDATION (a coordinate
 * must be real and in range).
 *
 * Dependency-free: importable from Vite client code, Vercel Edge bundles,
 * server handlers, and tsx tests alike.
 */

/** Narrow to a plain object; arrays and null are rejected. */
export function asRecord(value: unknown): Record<string, unknown> | null {
  return typeof value === 'object' && value !== null && !Array.isArray(value)
    ? (value as Record<string, unknown>)
    : null;
}

/** Always yields an iterable — a non-array becomes empty rather than throwing. */
export function asArray(value: unknown): unknown[] {
  return Array.isArray(value) ? value : [];
}

/** Finite number, accepting a numeric string; anything else is null. */
export function finiteNumber(value: unknown): number | null {
  if (typeof value === 'number') return Number.isFinite(value) ? value : null;
  if (typeof value === 'string' && value.trim() !== '') {
    const parsed = Number(value);
    return Number.isFinite(parsed) ? parsed : null;
  }
  return null;
}

/** Trimmed string, or '' for anything non-string or whitespace-only. */
export function nonEmptyString(value: unknown): string {
  return typeof value === 'string' && value.trim() !== '' ? value.trim() : '';
}

/**
 * A coordinate pair usable as a real position: both components present, in
 * range, and not null island — (0, 0) is overwhelmingly a missing-data
 * artifact in these feeds rather than a location in the Gulf of Guinea.
 *
 * Note the exposure adapters deliberately do NOT apply this: they enrich
 * whatever the caller asks about, and rejecting a point there would silently
 * drop an event rather than decline to place it on a grid.
 */
export function usableCoord(lat: number | null, lon: number | null): lat is number {
  return (
    lat !== null && lon !== null &&
    lat >= -90 && lat <= 90 &&
    lon >= -180 && lon <= 180 &&
    !(lat === 0 && lon === 0)
  );
}

/** Read a nested `{ location: { latitude, longitude } }` pair. */
export function nestedLocation(record: Record<string, unknown>): { lat: number | null; lon: number | null } {
  const location = asRecord(record.location);
  return {
    lat: finiteNumber(location?.latitude),
    lon: finiteNumber(location?.longitude),
  };
}