Skip to content

Commit e97d96b

Browse files
committed
chore: enable noUncheckedIndexedAccess tsconfig setting
- aggregate.ts: extract times[i] in loop with undefined guard, fix leadHoursAt signature, guard used[id] in for-in renorm loops - aggregate.test.ts: use optional chaining (out[N]?.prop) for index accesses; use toBeDefined() + narrowing guard in wind test - chartHelpers.ts: guard times[i], times[0], sunrise[0], sunset[i] with explicit undefined checks - useForecast.ts: guard times[0] with early return null in both hourly and daily computed properties - useUnits.ts: add ?? fallback on COMPASS[idx] - HourlyChart.vue: guard times[idx] and precP in tooltip formatter Assisted-by: Zed:claude-sonnet-4-6
1 parent 93b4619 commit e97d96b

7 files changed

Lines changed: 59 additions & 32 deletions

File tree

src/components/HourlyChart.vue

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,13 +76,14 @@ const option = computed<EChartsOption>(() => {
7676
const idx = labels.indexOf(t);
7777
const tempP = temps[idx];
7878
const precP = precips[idx];
79-
if (!tempP) return "";
80-
const date = new Date(times[idx]);
79+
const timeStr = times[idx];
80+
if (!tempP || timeStr === undefined) return "";
81+
const date = new Date(timeStr);
8182
const header = date.toLocaleString([], { weekday: "short", hour: "2-digit", minute: "2-digit" });
8283
const tempStr =
8384
`<span style="color:#fda4af">${formatTemp.value(tempP.value, 1)}</span>` +
8485
` <span style="color:#94a3b8">± ${formatTemp.value(tempP.stdDev, 1).replace(/°[CF]/, "")} ${tempUnit.value}</span>`;
85-
const precStr = precP.value > 0.05 ? `<br/><span style="color:#7dd3fc">${formatPrecip.value(precP.value, 1)}</span>` : "";
86+
const precStr = precP != null && precP.value > 0.05 ? `<br/><span style="color:#7dd3fc">${formatPrecip.value(precP.value, 1)}</span>` : "";
8687
return `<div style="font-weight:600;margin-bottom:4px">${header}</div>${tempStr}${precStr}`;
8788
},
8889
},

src/components/chartHelpers.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
/** First index in `times` whose timestamp is at or after `nowStr`. */
77
export function findNowIndex(times: string[], nowStr: string): number {
88
for (let i = 0; i < times.length; i++) {
9-
if (times[i] >= nowStr) return i;
9+
const t = times[i];
10+
if (t !== undefined && t >= nowStr) return i;
1011
}
1112
return -1;
1213
}
@@ -20,17 +21,22 @@ function isoToHourIndex(iso: string, baseMs: number, count: number): number {
2021
/** Build [startIdx, endIdx] pairs covering night hours within the visible window. */
2122
export function buildNightRanges(times: string[], sunrise: string[] | undefined, sunset: string[] | undefined): Array<[number, number]> {
2223
if (!times.length || !sunrise?.length || !sunset?.length) return [];
23-
const baseMs = new Date(times[0]).getTime();
24+
const firstTime = times[0];
25+
const firstSunrise = sunrise[0];
26+
if (firstTime === undefined || firstSunrise === undefined) return [];
27+
const baseMs = new Date(firstTime).getTime();
2428
const count = times.length;
2529
const ranges: Array<[number, number]> = [];
2630

2731
// Pre-dawn on the first day.
28-
const firstRise = isoToHourIndex(sunrise[0], baseMs, count);
32+
const firstRise = isoToHourIndex(firstSunrise, baseMs, count);
2933
if (firstRise > 0) ranges.push([0, firstRise]);
3034

3135
// Sunset of day i → sunrise of day i+1.
3236
for (let i = 0; i < sunset.length; i++) {
33-
const setIdx = isoToHourIndex(sunset[i], baseMs, count);
37+
const sunsetTime = sunset[i];
38+
if (sunsetTime === undefined) continue;
39+
const setIdx = isoToHourIndex(sunsetTime, baseMs, count);
3440
const nextRiseIso = sunrise[i + 1];
3541
const endIdx = nextRiseIso ? isoToHourIndex(nextRiseIso, baseMs, count) : count - 1;
3642
if (endIdx > setIdx) ranges.push([setIdx, endIdx]);

src/composables/useForecast.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,9 @@ export function useForecast(location: Ref<Location>): UseForecastReturn {
101101
const data = raw.value;
102102
if (!data) return null;
103103
const times = data.hourly.time;
104-
const baseTime = new Date(times[0]);
104+
const firstHourlyTime = times[0];
105+
if (firstHourlyTime === undefined) return null;
106+
const baseTime = new Date(firstHourlyTime);
105107
const series: Record<HourlyVar, AggregatePoint[]> = {} as never;
106108
const confidence: Record<HourlyVar, number[]> = {} as never;
107109
const perModel: Record<HourlyVar, Record<string, (number | null)[]>> = {} as never;
@@ -125,7 +127,9 @@ export function useForecast(location: Ref<Location>): UseForecastReturn {
125127
const data = raw.value;
126128
if (!data) return null;
127129
const times = data.daily.time;
128-
const baseTime = new Date(times[0]);
130+
const firstDailyTime = times[0];
131+
if (firstDailyTime === undefined) return null;
132+
const baseTime = new Date(firstDailyTime);
129133
const series: Record<DailyVar, AggregatePoint[]> = {} as never;
130134
const confidence: Record<DailyVar, number[]> = {} as never;
131135
const perModel: Record<DailyVar, Record<string, (number | null)[]>> = {} as never;

src/composables/useUnits.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export function formatPercent(v: number | null | undefined): string {
1717
export function compassPoint(deg: number | null | undefined): string {
1818
if (deg == null || Number.isNaN(deg)) return "–";
1919
const idx = Math.round((((deg % 360) + 360) % 360) / 45) % 8;
20-
return COMPASS[idx];
20+
return COMPASS[idx] ?? "–";
2121
}
2222

2323
export function useUnits() {

src/domain/aggregate.test.ts

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ describe("aggregateSeries (temperature)", () => {
2929
baseTime,
3030
});
3131
expect(out).toHaveLength(4);
32-
expect(out[0].value).toBeCloseTo(10, 5);
33-
expect(out[3].value).toBeCloseTo(13, 5);
32+
expect(out[0]?.value).toBeCloseTo(10, 5);
33+
expect(out[3]?.value).toBeCloseTo(13, 5);
3434
for (const p of out) expect(p.stdDev).toBeCloseTo(0, 5);
3535
});
3636

@@ -50,9 +50,9 @@ describe("aggregateSeries (temperature)", () => {
5050
lon: PARIS.lon,
5151
baseTime,
5252
});
53-
expect(out[0].stdDev).toBeGreaterThan(0);
54-
expect(out[0].value).toBeGreaterThan(10);
55-
expect(out[0].value).toBeLessThan(14);
53+
expect(out[0]?.stdDev).toBeGreaterThan(0);
54+
expect(out[0]?.value).toBeGreaterThan(10);
55+
expect(out[0]?.value).toBeLessThan(14);
5656
});
5757

5858
it("handles null per-model values without crashing", () => {
@@ -71,7 +71,7 @@ describe("aggregateSeries (temperature)", () => {
7171
lon: PARIS.lon,
7272
baseTime,
7373
});
74-
expect(out[0].value).toBeCloseTo(10, 5);
74+
expect(out[0]?.value).toBeCloseTo(10, 5);
7575
});
7676
});
7777

@@ -98,7 +98,10 @@ describe("aggregateSeries (wind_direction_10m)", () => {
9898
},
9999
);
100100
// Allow either side of the wrap.
101-
const v = out[0].value;
101+
const p0 = out[0];
102+
expect(p0).toBeDefined();
103+
if (!p0) return;
104+
const v = p0.value;
102105
const distFromNorth = Math.min(v, 360 - v);
103106
expect(distFromNorth).toBeLessThan(2);
104107
});
@@ -120,8 +123,8 @@ describe("aggregateSeries (wind_direction_10m)", () => {
120123
baseTime,
121124
},
122125
);
123-
expect(out[0].value).toBeCloseTo(180, 0);
124-
expect(out[0].stdDev).toBeLessThan(10);
126+
expect(out[0]?.value).toBeCloseTo(180, 0);
127+
expect(out[0]?.stdDev).toBeLessThan(10);
125128
});
126129

127130
it("reports a large angular stddev when models are opposite", () => {
@@ -142,7 +145,7 @@ describe("aggregateSeries (wind_direction_10m)", () => {
142145
},
143146
);
144147
// Mean resultant length → 0, so circular stddev should be huge.
145-
expect(out[0].stdDev).toBeGreaterThan(90);
148+
expect(out[0]?.stdDev).toBeGreaterThan(90);
146149
});
147150
});
148151

@@ -165,6 +168,6 @@ describe("aggregateSeries (weather_code)", () => {
165168
baseTime,
166169
});
167170
// Should land somewhere in the rain group.
168-
expect([61, 63, 80]).toContain(out[0].value);
171+
expect([61, 63, 80]).toContain(out[0]?.value);
169172
});
170173
});

src/domain/aggregate.ts

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,11 @@ export interface AggregateOptions {
2727
baseTime: Date;
2828
}
2929

30-
function leadHoursAt(times: string[], i: number, baseTime: Date): number {
30+
function leadHoursAt(time: string, baseTime: Date): number {
3131
// open-meteo returns timezone-shifted ISO strings without a TZ marker.
3232
// Both the times array and our cursor are in the same local frame, so a
3333
// direct millisecond diff is correct.
34-
const t = new Date(times[i]).getTime();
34+
const t = new Date(time).getTime();
3535
return Math.max(0, (t - baseTime.getTime()) / 3_600_000);
3636
}
3737

@@ -53,11 +53,15 @@ function weightedMean(perModel: ModelSamples, weights: Map<string, number>): { m
5353
let varSum = 0;
5454
for (const id in used) {
5555
const v = perModel[id];
56-
if (v == null) continue;
57-
varSum += (used[id] / totalW) * (v - mean) ** 2;
56+
const w = used[id];
57+
if (v == null || w === undefined) continue;
58+
varSum += (w / totalW) * (v - mean) ** 2;
5859
}
5960
// Renormalize so the exposed weights sum to 1 even after dropping nulls.
60-
for (const id in used) used[id] = used[id] / totalW;
61+
for (const id in used) {
62+
const w = used[id];
63+
if (w !== undefined) used[id] = w / totalW;
64+
}
6165
return { mean, stdDev: Math.sqrt(varSum), effectiveWeights: used };
6266
}
6367

@@ -88,7 +92,10 @@ function weightedCircularMean(perModel: ModelSamples, weights: Map<string, numbe
8892
const R = Math.min(1, Math.sqrt(mx * mx + my * my));
8993
// Circular standard deviation in radians: sqrt(-2 * ln(R)). Convert to degrees.
9094
const stdDev = R > 0 ? (Math.sqrt(-2 * Math.log(R)) * 180) / Math.PI : 180;
91-
for (const id in used) used[id] = used[id] / totalW;
95+
for (const id in used) {
96+
const w = used[id];
97+
if (w !== undefined) used[id] = w / totalW;
98+
}
9299
return { mean, stdDev, effectiveWeights: used };
93100
}
94101

@@ -125,7 +132,10 @@ function severityWeightedMode(perModel: ModelSamples, weights: Map<string, numbe
125132
bestCodeW = w;
126133
}
127134
}
128-
for (const id in used) used[id] = used[id] / totalW;
135+
for (const id in used) {
136+
const w = used[id];
137+
if (w !== undefined) used[id] = w / totalW;
138+
}
129139
return { code: bestCode, effectiveWeights: used };
130140
}
131141

@@ -135,7 +145,9 @@ export function aggregateSeries(times: string[], series: Record<string, (number
135145
const { variable, models, lat, lon, baseTime } = opts;
136146
const result: AggregatePoint[] = [];
137147
for (let i = 0; i < times.length; i++) {
138-
const leadH = leadHoursAt(times, i, baseTime);
148+
const timeStr = times[i];
149+
if (timeStr === undefined) continue;
150+
const leadH = leadHoursAt(timeStr, baseTime);
139151
const weights = normalizedWeights(models, leadH, lat, lon, variable);
140152
const perModel: ModelSamples = {};
141153
for (const m of models) {
@@ -150,7 +162,7 @@ export function aggregateSeries(times: string[], series: Record<string, (number
150162
}
151163
const { code, effectiveWeights } = severityWeightedMode(perModel, weights);
152164
result.push({
153-
time: times[i],
165+
time: timeStr,
154166
value: code,
155167
stdDev: 0,
156168
weights: effectiveWeights,
@@ -159,7 +171,7 @@ export function aggregateSeries(times: string[], series: Record<string, (number
159171
} else if (variable === "wind_direction_10m") {
160172
const { mean, stdDev, effectiveWeights } = weightedCircularMean(perModel, weights);
161173
result.push({
162-
time: times[i],
174+
time: timeStr,
163175
value: mean,
164176
stdDev,
165177
weights: effectiveWeights,
@@ -168,7 +180,7 @@ export function aggregateSeries(times: string[], series: Record<string, (number
168180
} else {
169181
const { mean, stdDev, effectiveWeights } = weightedMean(perModel, weights);
170182
result.push({
171-
time: times[i],
183+
time: timeStr,
172184
value: mean,
173185
stdDev,
174186
weights: effectiveWeights,

tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"noFallthroughCasesInSwitch": true,
66
"noUnusedLocals": true,
77
"noUnusedParameters": true,
8+
"noUncheckedIndexedAccess": true,
89
"types": ["vite/client", "vite-plugin-pwa/client"],
910
"paths": {
1011
"@/*": ["./src/*"]

0 commit comments

Comments
 (0)