const std = @import("std"); const zeit = @import("zeit"); const types = @import("../weather/types.zig"); const emoji = @import("emoji.zig"); const utils = @import("utils.zig"); const Moon = @import("../Moon.zig"); const Astronomical = @import("../Astronomical.zig"); const TimeZoneOffsets = @import("../location/timezone_offsets.zig"); const Coordinates = @import("../Coordinates.zig"); pub fn render(writer: *std.Io.Writer, weather: types.WeatherData, format: []const u8, use_imperial: bool) !void { var i: usize = 0; while (i < format.len) { if (format[i] == '%' and i + 1 < format.len) { const code = format[i + 1]; switch (code) { 'c' => try writer.writeAll(emoji.getWeatherEmoji(weather.current.weather_code)), 'C' => try writer.writeAll(weather.current.condition), 'h' => try writer.print("{d}%", .{weather.current.humidity}), 't' => { const temp = if (use_imperial) weather.current.tempFahrenheit() else weather.current.temp_c; const unit = if (use_imperial) "°F" else "°C"; const sign: u8 = if (temp >= 0) '+' else 0; if (sign != 0) { try writer.print("{c}{d:.1}{s}", .{ sign, temp, unit }); } else { try writer.print("{d:.1}{s}", .{ temp, unit }); } }, 'f' => { const temp = if (use_imperial) weather.current.tempFahrenheit() else weather.current.temp_c; const unit = if (use_imperial) "°F" else "°C"; const sign: u8 = if (temp >= 0) '+' else 0; if (sign != 0) { try writer.print("{c}{d:.1}{s}", .{ sign, temp, unit }); } else { try writer.print("{d:.1}{s}", .{ temp, unit }); } }, 'w' => { const wind = if (use_imperial) weather.current.windMph() else weather.current.wind_kph; const unit = if (use_imperial) "mph" else "km/h"; try writer.print("{d:.0} {s} {s}", .{ wind, unit, utils.degreeToDirection(weather.current.wind_deg) }); }, 'l' => try writer.writeAll(weather.locationDisplayName()), 'p' => { const precip = if (use_imperial) weather.current.precip_mm * 0.0393701 else weather.current.precip_mm; const unit = if (use_imperial) "in" else "mm"; try writer.print("{d:.1} {s}", .{ precip, unit }); }, 'P' => { const pressure = if (use_imperial) weather.current.pressure_mb * 0.02953 else weather.current.pressure_mb; const unit = if (use_imperial) "inHg" else "hPa"; try writer.print("{d:.2} {s}", .{ pressure, unit }); }, 'm' => { const now = try nowAt(weather.coords); const moon = Moon.getPhase(now); try writer.writeAll(moon.emoji()); }, 'M' => { const now = try nowAt(weather.coords); const moon = Moon.getPhase(now); try writer.print("{d}", .{moon.day()}); }, 'o' => try writer.print("0%", .{}), // Probability of precipitation placeholder 'D', 'S', 'z', 's', 'd' => { // Again...we only need approximate, because we simply need // to make sure the day is correct for this. Even a day off // should actually be ok. Unix timestamp is always UTC, // so we convert to local const now = try nowAt(weather.coords); const astro = Astronomical.init( weather.coords.latitude, weather.coords.longitude, now, ); const utc_time = switch (code) { 'D' => astro.dawn, 'S' => astro.sunrise, 'z' => astro.zenith, 's' => astro.sunset, 'd' => astro.dusk, else => unreachable, }; try writer.print("{f}", .{try utc_time.adjustTimeToLocal(weather.coords)}); }, '%' => try writer.print("%", .{}), 'n' => try writer.print("\n", .{}), else => { try writer.print("%{c}", .{code}); }, } i += 2; } else { try writer.writeByte(format[i]); i += 1; } } } fn nowAt(coords: Coordinates) !i64 { const now = try zeit.instant(.{}); const offset = TimeZoneOffsets.getTimezoneOffset(coords); const new = if (offset >= 0) try now.add(.{ .minutes = @abs(offset) }) else try now.subtract(.{ .minutes = @abs(offset) }); return new.unixTimestamp(); } const test_weather = types.WeatherData{ // SAFETY: allocator unused in these tests .allocator = undefined, .location = "Test", .display_name = null, .coords = .{ .latitude = 0, .longitude = 0 }, .current = .{ .temp_c = 10, .feels_like_c = 10, .condition = "Clear", .weather_code = .clear, .humidity = 50, .wind_kph = 10, .wind_deg = 0, .pressure_mb = 1013, .precip_mm = 0, .visibility_km = 10, }, .forecast = &.{}, }; test "render custom format with location and temp" { const allocator = std.testing.allocator; const weather = types.WeatherData{ .location = "London", .coords = .{ .latitude = 0, .longitude = 0 }, .current = .{ .temp_c = 7.0, .feels_like_c = 7.0, .condition = "Overcast", .weather_code = .clouds_overcast, .humidity = 76, .wind_kph = 11.0, .wind_deg = 22.5, .pressure_mb = 1019.0, .precip_mm = 0.0, .visibility_km = null, }, .forecast = &[_]types.ForecastDay{}, .allocator = allocator, }; var output_buf: [1024]u8 = undefined; var writer = std.Io.Writer.fixed(&output_buf); try render(&writer, weather, "%l: %c %t", false); const output = output_buf[0..writer.end]; try std.testing.expect(std.mem.indexOf(u8, output, "London") != null); try std.testing.expect(std.mem.indexOf(u8, output, "+7.0°C") != null); } test "render custom format with newline" { const allocator = std.testing.allocator; const weather = types.WeatherData{ .location = "Paris", .coords = .{ .latitude = 0, .longitude = 0 }, .current = .{ .temp_c = 10.0, .feels_like_c = 10.0, .condition = "Clear", .weather_code = .clear, .humidity = 65, .wind_kph = 8.0, .wind_deg = 90.0, .pressure_mb = 1020.0, .precip_mm = 0.0, .visibility_km = null, }, .forecast = &[_]types.ForecastDay{}, .allocator = allocator, }; var output_buf: [1024]u8 = undefined; var writer = std.Io.Writer.fixed(&output_buf); try render(&writer, weather, "%l%n%C", false); const output = output_buf[0..writer.end]; try std.testing.expect(std.mem.indexOf(u8, output, "Paris\nClear") != null); } test "render custom format with humidity and pressure" { const allocator = std.testing.allocator; const weather = types.WeatherData{ .location = "Berlin", .coords = .{ .latitude = 0, .longitude = 0 }, .current = .{ .temp_c = 5.0, .feels_like_c = 5.0, .condition = "Cloudy", .weather_code = .clouds_overcast, .humidity = 85, .wind_kph = 12.0, .wind_deg = 270.0, .pressure_mb = 1012.0, .precip_mm = 0.2, .visibility_km = null, }, .forecast = &[_]types.ForecastDay{}, .allocator = allocator, }; var output_buf: [1024]u8 = undefined; var writer = std.Io.Writer.fixed(&output_buf); try render(&writer, weather, "Humidity: %h, Pressure: %P", false); const output = output_buf[0..writer.end]; try std.testing.expect(std.mem.indexOf(u8, output, "85%") != null); try std.testing.expect(std.mem.indexOf(u8, output, "1012") != null); } test "render custom format with imperial units" { const allocator = std.testing.allocator; const weather = types.WeatherData{ .location = "NYC", .coords = .{ .latitude = 0, .longitude = 0 }, .current = .{ .temp_c = 10.0, .feels_like_c = 10.0, .condition = "Clear", .weather_code = .clear, .humidity = 60, .wind_kph = 16.0, .wind_deg = 0.0, .pressure_mb = 1013.0, .precip_mm = 2.5, .visibility_km = null, }, .forecast = &[_]types.ForecastDay{}, .allocator = allocator, }; var output_buf: [1024]u8 = undefined; var writer = std.Io.Writer.fixed(&output_buf); try render(&writer, weather, "%t %w %p", true); const output = output_buf[0..writer.end]; try std.testing.expect(std.mem.indexOf(u8, output, "+50.0°F") != null); try std.testing.expect(std.mem.indexOf(u8, output, "mph") != null); try std.testing.expect(std.mem.indexOf(u8, output, "in") != null); } test "render custom format with feels like temp" { var output_buf: [1024]u8 = undefined; var writer = std.Io.Writer.fixed(&output_buf); try render(&writer, test_weather, "%f", false); const output = output_buf[0..writer.end]; try std.testing.expectEqualStrings("+10.0°C", output); } test "render custom format with moon phase" { var output_buf: [1024]u8 = undefined; var writer = std.Io.Writer.fixed(&output_buf); try render(&writer, test_weather, "%m", false); const output = output_buf[0..writer.end]; try std.testing.expectEqualStrings("🌗", output); } test "render custom format with moon day" { var output_buf: [1024]u8 = undefined; var writer = std.Io.Writer.fixed(&output_buf); try render(&writer, test_weather, "%M", false); const output = output_buf[0..writer.end]; try std.testing.expectEqualStrings("21", output); } test "render custom format with astronomical dawn" { var test_weather_astro = test_weather; test_weather_astro.coords = .{ .latitude = 45.0, .longitude = -122.0 }; var output_buf: [1024]u8 = undefined; var writer = std.Io.Writer.fixed(&output_buf); try render(&writer, test_weather_astro, "%D", false); const output = output_buf[0..writer.end]; try std.testing.expectEqualStrings("07:12", output); } test "render custom format with astronomical sunrise" { var test_weather_astro = test_weather; test_weather_astro.coords = .{ .latitude = 45.0, .longitude = -122.0 }; var output_buf: [1024]u8 = undefined; var writer = std.Io.Writer.fixed(&output_buf); try render(&writer, test_weather_astro, "%S", false); const output = output_buf[0..writer.end]; try std.testing.expectEqualStrings("07:45", output); } test "render custom format with astronomical zenith" { var test_weather_astro = test_weather; test_weather_astro.coords = .{ .latitude = 45.0, .longitude = -122.0 }; var output_buf: [1024]u8 = undefined; var writer = std.Io.Writer.fixed(&output_buf); try render(&writer, test_weather_astro, "%z", false); const output = output_buf[0..writer.end]; try std.testing.expectEqualStrings("12:14", output); } test "render custom format with astronomical sunset" { var test_weather_astro = test_weather; test_weather_astro.coords = .{ .latitude = 45.0, .longitude = -122.0 }; var output_buf: [1024]u8 = undefined; var writer = std.Io.Writer.fixed(&output_buf); try render(&writer, test_weather_astro, "%s", false); const output = output_buf[0..writer.end]; try std.testing.expectEqualStrings("16:44", output); } test "render custom format with astronomical dusk" { var test_weather_astro = test_weather; test_weather_astro.coords = .{ .latitude = 45.0, .longitude = -122.0 }; var output_buf: [1024]u8 = undefined; var writer = std.Io.Writer.fixed(&output_buf); try render(&writer, test_weather_astro, "%d", false); const output = output_buf[0..writer.end]; try std.testing.expectEqualStrings("17:17", output); } test "render custom format with percent sign" { var output_buf: [1024]u8 = undefined; var writer = std.Io.Writer.fixed(&output_buf); try render(&writer, test_weather, "%%", false); const output = output_buf[0..writer.end]; try std.testing.expectEqualStrings("%", output); }