From 115af96c8ad7c7ee0e6cd09580112b92f66e2175 Mon Sep 17 00:00:00 2001 From: Emil Lerch Date: Thu, 8 Jan 2026 14:47:31 -0800 Subject: [PATCH] update line and push custom to custom --- src/http/handler.zig | 36 +++-- src/render/Custom.zig | 126 +++++++++++++++++ src/render/Line.zig | 314 ++++++++++++++++++------------------------ 3 files changed, 290 insertions(+), 186 deletions(-) diff --git a/src/http/handler.zig b/src/http/handler.zig index 2938f73..1e3df08 100644 --- a/src/http/handler.zig +++ b/src/http/handler.zig @@ -143,19 +143,37 @@ fn handleWeatherInternal( if (params.format) |fmt| { // Anything except the json will be plain text res.content_type = .TEXT; + if (std.mem.eql(u8, fmt, "1")) { + try Line.render(res.writer(), weather, .@"1", render_options.use_imperial); + return; + } + if (std.mem.eql(u8, fmt, "2")) { + try Line.render(res.writer(), weather, .@"2", render_options.use_imperial); + return; + } + if (std.mem.eql(u8, fmt, "3")) { + try Line.render(res.writer(), weather, .@"3", render_options.use_imperial); + return; + } + if (std.mem.eql(u8, fmt, "4")) { + try Line.render(res.writer(), weather, .@"4", render_options.use_imperial); + return; + } if (std.mem.eql(u8, fmt, "j1")) { res.content_type = .JSON; // reset to json try Json.render(res.writer(), weather); - } else if (std.mem.eql(u8, fmt, "p1")) { - try Prometheus.render(res.writer(), weather); - } else if (std.mem.eql(u8, fmt, "v2")) { - try V2.render(res.writer(), weather, render_options.use_imperial); - } else if (std.mem.startsWith(u8, fmt, "%")) { - try Custom.render(res.writer(), weather, fmt, render_options.use_imperial); - } else { - // fall back to line if we don't understand the format parameter - try Line.render(res.writer(), weather, fmt, render_options.use_imperial); + return; } + if (std.mem.eql(u8, fmt, "p1")) { + try Prometheus.render(res.writer(), weather); + return; + } + if (std.mem.eql(u8, fmt, "v2")) { + try V2.render(res.writer(), weather, render_options.use_imperial); + return; + } + // Everything else goes to Custom renderer + try Custom.render(res.writer(), weather, fmt, render_options.use_imperial); } else { // No specific format selected, we'll provide Formatted output in either // text (ansi/plain) or html diff --git a/src/render/Custom.zig b/src/render/Custom.zig index e396e60..04e246f 100644 --- a/src/render/Custom.zig +++ b/src/render/Custom.zig @@ -109,6 +109,27 @@ fn nowAt(coords: Coordinates) !i64 { 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; @@ -240,3 +261,108 @@ test "render custom format with imperial units" { 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); +} diff --git a/src/render/Line.zig b/src/render/Line.zig index 5942b86..13dda6a 100644 --- a/src/render/Line.zig +++ b/src/render/Line.zig @@ -3,170 +3,105 @@ const types = @import("../weather/types.zig"); const emoji = @import("emoji.zig"); const utils = @import("utils.zig"); -pub fn render(writer: *std.Io.Writer, data: types.WeatherData, format: []const u8, use_imperial: bool) !void { - if (std.mem.eql(u8, format, "1")) { - const temp = if (use_imperial) data.current.tempFahrenheit() else data.current.temp_c; - const unit = if (use_imperial) "°F" else "°C"; - try writer.print("{s}: {s} {d:.0}{s}", .{ - data.location, - emoji.getWeatherEmoji(data.current.weather_code), - temp, - unit, - }); - } else if (std.mem.eql(u8, format, "2")) { - const temp = if (use_imperial) data.current.tempFahrenheit() else data.current.temp_c; - const unit = if (use_imperial) "°F" else "°C"; - const wind = if (use_imperial) data.current.windMph() else data.current.wind_kph; - const wind_unit = if (use_imperial) "mph" else "km/h"; - try writer.print("{s}: {s} {d:.0}{s} {s}{s}{d:.0}{s}", .{ - data.location, - emoji.getWeatherEmoji(data.current.weather_code), - temp, - unit, - "🌬️", - utils.degreeToDirection(data.current.wind_deg), - wind, - wind_unit, - }); - } else if (std.mem.eql(u8, format, "3")) { - const temp = if (use_imperial) data.current.tempFahrenheit() else data.current.temp_c; - const unit = if (use_imperial) "°F" else "°C"; - const wind = if (use_imperial) data.current.windMph() else data.current.wind_kph; - const wind_unit = if (use_imperial) "mph" else "km/h"; - try writer.print("{s}: {s} {d:.0}{s} {s}{s}{d:.0}{s} {s}{d}%%", .{ - data.location, - emoji.getWeatherEmoji(data.current.weather_code), - temp, - unit, - "🌬️", - utils.degreeToDirection(data.current.wind_deg), - wind, - wind_unit, - "💧", - data.current.humidity, - }); - } else if (std.mem.eql(u8, format, "4")) { - const temp = if (use_imperial) data.current.tempFahrenheit() else data.current.temp_c; - const unit = if (use_imperial) "°F" else "°C"; - const wind = if (use_imperial) data.current.windMph() else data.current.wind_kph; - const wind_unit = if (use_imperial) "mph" else "km/h"; - try writer.print("{s}: {s} {d:.0}{s} {s}{s}{d:.0}{s} {s}{d}%% {s}", .{ - data.location, - emoji.getWeatherEmoji(data.current.weather_code), - temp, - unit, - "🌬️", - utils.degreeToDirection(data.current.wind_deg), - wind, - wind_unit, - "💧", - data.current.humidity, - "☀️", - }); - } else { - try renderCustom(writer, data, format, use_imperial); +const Format = enum(u3) { + @"1" = 1, + @"2" = 2, + @"3" = 3, + @"4" = 4, +}; + +pub fn render(writer: *std.Io.Writer, data: types.WeatherData, format: Format, use_imperial: bool) !void { + switch (format) { + .@"1" => { + const temp = if (use_imperial) data.current.tempFahrenheit() else data.current.temp_c; + const unit = if (use_imperial) "°F" else "°C"; + try writer.print("{s}: {s} {d:.0}{s}", .{ + data.location, + emoji.getWeatherEmoji(data.current.weather_code), + temp, + unit, + }); + }, + .@"2" => { + const temp = if (use_imperial) data.current.tempFahrenheit() else data.current.temp_c; + const unit = if (use_imperial) "°F" else "°C"; + const wind = if (use_imperial) data.current.windMph() else data.current.wind_kph; + const wind_unit = if (use_imperial) "mph" else "km/h"; + try writer.print("{s}: {s} {d:.0}{s} {s}{s}{d:.0}{s}", .{ + data.location, + emoji.getWeatherEmoji(data.current.weather_code), + temp, + unit, + "🌬️", + utils.degreeToDirection(data.current.wind_deg), + wind, + wind_unit, + }); + }, + .@"3" => { + const temp = if (use_imperial) data.current.tempFahrenheit() else data.current.temp_c; + const unit = if (use_imperial) "°F" else "°C"; + const wind = if (use_imperial) data.current.windMph() else data.current.wind_kph; + const wind_unit = if (use_imperial) "mph" else "km/h"; + try writer.print("{s}: {s} {d:.0}{s} {s}{s}{d:.0}{s} {s}{d}%%", .{ + data.location, + emoji.getWeatherEmoji(data.current.weather_code), + temp, + unit, + "🌬️", + utils.degreeToDirection(data.current.wind_deg), + wind, + wind_unit, + "💧", + data.current.humidity, + }); + }, + .@"4" => { + const temp = if (use_imperial) data.current.tempFahrenheit() else data.current.temp_c; + const unit = if (use_imperial) "°F" else "°C"; + const wind = if (use_imperial) data.current.windMph() else data.current.wind_kph; + const wind_unit = if (use_imperial) "mph" else "km/h"; + try writer.print("{s}: {s} {d:.0}{s} {s}{s}{d:.0}{s} {s}{d}%% {s}", .{ + data.location, + emoji.getWeatherEmoji(data.current.weather_code), + temp, + unit, + "🌬️", + utils.degreeToDirection(data.current.wind_deg), + wind, + wind_unit, + "💧", + data.current.humidity, + "☀️", + }); + }, } } -fn renderCustom(writer: *std.Io.Writer, data: 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(data.current.weather_code)), - 'C' => try writer.writeAll(data.current.condition), - 'h' => try writer.print("{d}", .{data.current.humidity}), - 't' => { - const temp = if (use_imperial) data.current.tempFahrenheit() else data.current.temp_c; - try writer.print("{d:.0}", .{temp}); - }, - 'f' => { - const temp = if (use_imperial) data.current.tempFahrenheit() else data.current.temp_c; - try writer.print("{d:.0}", .{temp}); - }, - 'w' => { - const wind = if (use_imperial) data.current.windMph() else data.current.wind_kph; - const wind_unit = if (use_imperial) "mph" else "km/h"; - try writer.print("{s}{d:.0}{s}", .{ utils.degreeToDirection(data.current.wind_deg), wind, wind_unit }); - }, - 'l' => try writer.writeAll(data.location), - 'p' => { - const precip = if (use_imperial) data.current.precip_mm * 0.0393701 else data.current.precip_mm; - try writer.print("{d:.1}", .{precip}); - }, - 'P' => { - const pressure = if (use_imperial) data.current.pressure_mb * 0.02953 else data.current.pressure_mb; - try writer.print("{d:.0}", .{pressure}); - }, - '%' => try writer.writeByte('%'), - else => { - try writer.writeByte('%'); - try writer.writeByte(code); - }, - } - i += 2; - } else { - try writer.writeByte(format[i]); - i += 1; - } - } -} +const test_data = types.WeatherData{ + .location = "London", + .coords = .{ .latitude = 0, .longitude = 0 }, + .current = .{ + .temp_c = 15.0, + .feels_like_c = 15.0, + .condition = "Clear", + .weather_code = .clear, + .humidity = 65, + .wind_kph = 10.0, + .wind_deg = 0.0, + .pressure_mb = 1013.0, + .precip_mm = 0.0, + .visibility_km = null, + }, + .forecast = &.{}, + .allocator = std.testing.allocator, +}; test "format 1" { - const data = types.WeatherData{ - .location = "London", - .coords = .{ .latitude = 0, .longitude = 0 }, - .current = .{ - .temp_c = 15.0, - .feels_like_c = 15.0, - .condition = "Clear", - .weather_code = .clear, - .humidity = 65, - .wind_kph = 10.0, - .wind_deg = 0.0, - .pressure_mb = 1013.0, - .precip_mm = 0.0, - .visibility_km = null, - }, - .forecast = &.{}, - .allocator = std.testing.allocator, - }; - var output_buf: [1024]u8 = undefined; var writer = std.Io.Writer.fixed(&output_buf); - try render(&writer, data, "1", false); - - const output = output_buf[0..writer.end]; - - try std.testing.expectEqualStrings("London: ☀️ 15°C", output); -} - -test "custom format" { - const data = types.WeatherData{ - .location = "London", - .coords = .{ .latitude = 0, .longitude = 0 }, - .current = .{ - .temp_c = 15.0, - .feels_like_c = 15.0, - .condition = "Clear", - .weather_code = .clear, - .humidity = 65, - .wind_kph = 10.0, - .wind_deg = 0.0, - .pressure_mb = 1013.0, - .precip_mm = 0.0, - .visibility_km = null, - }, - .forecast = &.{}, - .allocator = std.testing.allocator, - }; - - var output_buf: [1024]u8 = undefined; - var writer = std.Io.Writer.fixed(&output_buf); - - try render(&writer, data, "%l: %c %t°C", false); + try render(&writer, test_data, .@"1", false); const output = output_buf[0..writer.end]; @@ -174,31 +109,56 @@ test "custom format" { } test "format 2 with imperial units" { - const data = types.WeatherData{ - .location = "Portland", - .coords = .{ .latitude = 0, .longitude = 0 }, - .current = .{ - .temp_c = 10.0, - .feels_like_c = 10.0, - .condition = "Cloudy", - .weather_code = .clouds_overcast, - .humidity = 70, - .wind_kph = 20.0, - .wind_deg = 135.0, - .pressure_mb = 1013.0, - .precip_mm = 0.0, - .visibility_km = null, - }, - .forecast = &.{}, - .allocator = std.testing.allocator, - }; - var output_buf: [1024]u8 = undefined; var writer = std.Io.Writer.fixed(&output_buf); - try render(&writer, data, "2", true); + try render(&writer, test_data, .@"2", true); const output = output_buf[0..writer.end]; - try std.testing.expectEqualStrings("Portland: ☁️ 50°F 🌬️SE12mph", output); + try std.testing.expectEqualStrings("London: ☀️ 59°F 🌬️N6mph", output); +} + +test "format 3 with metric units" { + var output_buf: [1024]u8 = undefined; + var writer = std.Io.Writer.fixed(&output_buf); + + try render(&writer, test_data, .@"3", false); + + const output = output_buf[0..writer.end]; + + try std.testing.expectEqualStrings("London: ☀️ 15°C 🌬️N10km/h 💧65%%", output); +} + +test "format 3 with imperial units" { + var output_buf: [1024]u8 = undefined; + var writer = std.Io.Writer.fixed(&output_buf); + + try render(&writer, test_data, .@"3", true); + + const output = output_buf[0..writer.end]; + + try std.testing.expectEqualStrings("London: ☀️ 59°F 🌬️N6mph 💧65%%", output); +} + +test "format 4 with metric units" { + var output_buf: [1024]u8 = undefined; + var writer = std.Io.Writer.fixed(&output_buf); + + try render(&writer, test_data, .@"4", false); + + const output = output_buf[0..writer.end]; + + try std.testing.expectEqualStrings("London: ☀️ 15°C 🌬️N10km/h 💧65%% ☀️", output); +} + +test "format 4 with imperial units" { + var output_buf: [1024]u8 = undefined; + var writer = std.Io.Writer.fixed(&output_buf); + + try render(&writer, test_data, .@"4", true); + + const output = output_buf[0..writer.end]; + + try std.testing.expectEqualStrings("London: ☀️ 59°F 🌬️N6mph 💧65%% ☀️", output); }