const std = @import("std"); const weather_provider = @import("provider.zig"); const types = @import("types.zig"); const MetNo = @This(); allocator: std.mem.Allocator, pub fn init(allocator: std.mem.Allocator) !MetNo { return MetNo{ .allocator = allocator, }; } pub fn provider(self: *MetNo) weather_provider.WeatherProvider { return .{ .ptr = self, .vtable = &.{ .fetch = fetch, .deinit = deinitProvider, }, }; } fn fetch(ptr: *anyopaque, allocator: std.mem.Allocator, location: []const u8) !types.WeatherData { const self: *MetNo = @ptrCast(@alignCast(ptr)); // 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, "https://api.met.no/weatherapi/locationforecast/2.0/compact?lat={d:.4}&lon={d:.4}", .{ coords.lat, coords.lon }, ); defer self.allocator.free(url); // Fetch weather data from met.no API var client = std.http.Client{ .allocator = self.allocator }; defer client.deinit(); const uri = try std.Uri.parse(url); var response_buf: [1024 * 1024]u8 = undefined; var writer = std.Io.Writer.fixed(&response_buf); const result = try client.fetch(.{ .location = .{ .uri = uri }, .method = .GET, .response_writer = &writer, .extra_headers = &.{ .{ .name = "User-Agent", .value = "wttr.in-zig/1.0 github.com/chubin/wttr.in" }, }, }); if (result.status != .ok) { return error.WeatherApiFailed; } const response_body = response_buf[0..writer.end]; // Parse JSON response const parsed = try std.json.parseFromSlice( std.json.Value, allocator, response_body, .{}, ); defer parsed.deinit(); return try parseMetNoResponse(allocator, location, parsed.value); } fn deinitProvider(ptr: *anyopaque) void { const self: *MetNo = @ptrCast(@alignCast(ptr)); self.deinit(); } pub fn deinit(self: *MetNo) void { _ = self; } const Coords = struct { lat: f64, lon: f64, }; fn parseLocation(location: []const u8) !Coords { if (std.mem.indexOf(u8, location, ",")) |comma_idx| { 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 error.InvalidLocationFormat; } fn parseMetNoResponse(allocator: std.mem.Allocator, location: []const u8, json: std.json.Value) !types.WeatherData { const properties = json.object.get("properties") orelse return error.InvalidResponse; const timeseries = properties.object.get("timeseries") orelse return error.InvalidResponse; if (timeseries.array.items.len == 0) return error.InvalidResponse; const current = timeseries.array.items[0]; const data = current.object.get("data") orelse return error.InvalidResponse; const instant = data.object.get("instant") orelse return error.InvalidResponse; const details = instant.object.get("details") orelse return error.InvalidResponse; 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_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, symbolCodeToCondition(symbol_code)), .weather_code = symbolCodeToWeatherCode(symbol_code), .humidity = humidity, .wind_kph = wind_kph, .wind_dir = try allocator.dupe(u8, wind_dir), .pressure_mb = pressure_mb, .precip_mm = 0.0, }, .forecast = &.{}, .allocator = allocator, }; } fn symbolCodeToWeatherCode(symbol: []const u8) u16 { if (std.mem.indexOf(u8, symbol, "clearsky")) |_| return 113; if (std.mem.indexOf(u8, symbol, "fair")) |_| return 116; if (std.mem.indexOf(u8, symbol, "partlycloudy")) |_| return 116; if (std.mem.indexOf(u8, symbol, "cloudy")) |_| return 119; if (std.mem.indexOf(u8, symbol, "fog")) |_| return 143; if (std.mem.indexOf(u8, symbol, "rain")) |_| return 296; if (std.mem.indexOf(u8, symbol, "sleet")) |_| return 362; if (std.mem.indexOf(u8, symbol, "snow")) |_| return 338; 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")); }