diff --git a/zig/src/http/handler.zig b/zig/src/http/handler.zig index e6cc081..308b731 100644 --- a/zig/src/http/handler.zig +++ b/zig/src/http/handler.zig @@ -111,7 +111,10 @@ fn handleWeatherInternal( }; // Fetch weather using coordinates - const weather = opts.provider.fetch(allocator, location.name) catch |err| { + const coord_str = try std.fmt.allocPrint(allocator, "{d:.4},{d:.4}", .{ location.latitude, location.longitude }); + defer allocator.free(coord_str); + + const weather = opts.provider.fetch(allocator, coord_str) catch |err| { switch (err) { error.LocationNotFound => { res.status = 404; diff --git a/zig/src/weather/metno.zig b/zig/src/weather/metno.zig index b145f9a..b8c402d 100644 --- a/zig/src/weather/metno.zig +++ b/zig/src/weather/metno.zig @@ -24,7 +24,8 @@ pub const MetNo = struct { fn fetch(ptr: *anyopaque, allocator: std.mem.Allocator, location: []const u8) !types.WeatherData { const self: *MetNo = @ptrCast(@alignCast(ptr)); - const coords = try parseLocation(location); + // Parse location as "lat,lon" or use default + const coords = parseLocation(location) catch Coords{ .lat = 51.5074, .lon = -0.1278 }; const url = try std.fmt.allocPrint( self.allocator, @@ -33,6 +34,7 @@ pub const MetNo = struct { ); defer self.allocator.free(url); + // Fetch weather data from met.no API var client = std.http.Client{ .allocator = self.allocator }; defer client.deinit(); @@ -45,7 +47,7 @@ pub const MetNo = struct { .method = .GET, .response_writer = &writer, .extra_headers = &.{ - .{ .name = "User-Agent", .value = "wttr.in/1.0" }, + .{ .name = "User-Agent", .value = "wttr.in-zig/1.0 github.com/chubin/wttr.in" }, }, }); @@ -55,15 +57,16 @@ pub const MetNo = struct { const response_body = response_buf[0..writer.end]; - const json_data = try std.json.parseFromSlice( + // Parse JSON response + const parsed = try std.json.parseFromSlice( std.json.Value, - self.allocator, + allocator, response_body, .{}, ); - defer json_data.deinit(); + defer parsed.deinit(); - return try parseMetNoResponse(allocator, location, json_data.value); + return try parseMetNoResponse(allocator, location, parsed.value); } fn deinitProvider(ptr: *anyopaque) void { @@ -83,15 +86,14 @@ const Coords = struct { fn parseLocation(location: []const u8) !Coords { if (std.mem.indexOf(u8, location, ",")) |comma_idx| { - const lat_str = location[0..comma_idx]; - const lon_str = location[comma_idx + 1 ..]; + const lat_str = std.mem.trim(u8, location[0..comma_idx], " "); + const lon_str = std.mem.trim(u8, location[comma_idx + 1 ..], " "); return Coords{ .lat = try std.fmt.parseFloat(f64, lat_str), .lon = try std.fmt.parseFloat(f64, lon_str), }; } - - return Coords{ .lat = 51.5074, .lon = -0.1278 }; + return error.InvalidLocationFormat; } fn parseMetNoResponse(allocator: std.mem.Allocator, location: []const u8, json: std.json.Value) !types.WeatherData { @@ -107,25 +109,34 @@ fn parseMetNoResponse(allocator: std.mem.Allocator, location: []const u8, json: const temp_c = @as(f32, @floatCast(details.object.get("air_temperature").?.float)); const humidity = @as(u8, @intFromFloat(details.object.get("relative_humidity").?.float)); - const wind_kph = @as(f32, @floatCast(details.object.get("wind_speed").?.float * 3.6)); + const wind_speed_ms = details.object.get("wind_speed").?.float; + const wind_kph = @as(f32, @floatCast(wind_speed_ms * 3.6)); const pressure_mb = @as(f32, @floatCast(details.object.get("air_pressure_at_sea_level").?.float)); + // Get weather symbol const next_1h = data.object.get("next_1_hours"); const symbol_code = if (next_1h) |n1h| n1h.object.get("summary").?.object.get("symbol_code").?.string else "clearsky_day"; + // Get wind direction + const wind_from_deg = details.object.get("wind_from_direction"); + const wind_dir = if (wind_from_deg) |deg| + degreeToDirection(@as(f32, @floatCast(deg.float))) + else + "N"; + return types.WeatherData{ .location = try allocator.dupe(u8, location), .current = .{ .temp_c = temp_c, .temp_f = temp_c * 9.0 / 5.0 + 32.0, - .condition = try allocator.dupe(u8, symbol_code), + .condition = try allocator.dupe(u8, symbolCodeToCondition(symbol_code)), .weather_code = symbolCodeToWeatherCode(symbol_code), .humidity = humidity, .wind_kph = wind_kph, - .wind_dir = try allocator.dupe(u8, "N"), + .wind_dir = try allocator.dupe(u8, wind_dir), .pressure_mb = pressure_mb, .precip_mm = 0.0, }, @@ -146,3 +157,58 @@ fn symbolCodeToWeatherCode(symbol: []const u8) u16 { if (std.mem.indexOf(u8, symbol, "thunder")) |_| return 200; return 113; } + +fn symbolCodeToCondition(symbol: []const u8) []const u8 { + if (std.mem.indexOf(u8, symbol, "clearsky")) |_| return "Clear"; + if (std.mem.indexOf(u8, symbol, "fair")) |_| return "Fair"; + if (std.mem.indexOf(u8, symbol, "partlycloudy")) |_| return "Partly cloudy"; + if (std.mem.indexOf(u8, symbol, "cloudy")) |_| return "Cloudy"; + if (std.mem.indexOf(u8, symbol, "fog")) |_| return "Fog"; + if (std.mem.indexOf(u8, symbol, "rain")) |_| return "Rain"; + if (std.mem.indexOf(u8, symbol, "sleet")) |_| return "Sleet"; + if (std.mem.indexOf(u8, symbol, "snow")) |_| return "Snow"; + if (std.mem.indexOf(u8, symbol, "thunder")) |_| return "Thunderstorm"; + return "Clear"; +} + +fn degreeToDirection(deg: f32) []const u8 { + const normalized = @mod(deg + 22.5, 360.0); + const idx = @as(usize, @intFromFloat(normalized / 45.0)); + const directions = [_][]const u8{ "N", "NE", "E", "SE", "S", "SW", "W", "NW" }; + return directions[@min(idx, 7)]; +} + +test "parseLocation with valid coordinates" { + const coords = try parseLocation("51.5074,-0.1278"); + try std.testing.expectApproxEqAbs(@as(f64, 51.5074), coords.lat, 0.0001); + try std.testing.expectApproxEqAbs(@as(f64, -0.1278), coords.lon, 0.0001); +} + +test "parseLocation with whitespace" { + const coords = try parseLocation(" 40.7128 , -74.0060 "); + try std.testing.expectApproxEqAbs(@as(f64, 40.7128), coords.lat, 0.0001); + try std.testing.expectApproxEqAbs(@as(f64, -74.0060), coords.lon, 0.0001); +} + +test "parseLocation with invalid format" { + try std.testing.expectError(error.InvalidLocationFormat, parseLocation("London")); +} + +test "degreeToDirection" { + try std.testing.expectEqualStrings("N", degreeToDirection(0)); + try std.testing.expectEqualStrings("NE", degreeToDirection(45)); + try std.testing.expectEqualStrings("E", degreeToDirection(90)); + try std.testing.expectEqualStrings("SE", degreeToDirection(135)); + try std.testing.expectEqualStrings("S", degreeToDirection(180)); + try std.testing.expectEqualStrings("SW", degreeToDirection(225)); + try std.testing.expectEqualStrings("W", degreeToDirection(270)); + try std.testing.expectEqualStrings("NW", degreeToDirection(315)); +} + +test "symbolCodeToWeatherCode" { + try std.testing.expectEqual(@as(u16, 113), symbolCodeToWeatherCode("clearsky_day")); + try std.testing.expectEqual(@as(u16, 116), symbolCodeToWeatherCode("fair_night")); + try std.testing.expectEqual(@as(u16, 119), symbolCodeToWeatherCode("cloudy")); + try std.testing.expectEqual(@as(u16, 296), symbolCodeToWeatherCode("lightrain")); + try std.testing.expectEqual(@as(u16, 338), symbolCodeToWeatherCode("snow")); +}