update line and push custom to custom

This commit is contained in:
Emil Lerch 2026-01-08 14:47:31 -08:00
parent 894f7ca210
commit 115af96c8a
Signed by: lobo
GPG key ID: A7B62D657EF764F8
3 changed files with 290 additions and 186 deletions

View file

@ -143,19 +143,37 @@ fn handleWeatherInternal(
if (params.format) |fmt| { if (params.format) |fmt| {
// Anything except the json will be plain text // Anything except the json will be plain text
res.content_type = .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")) { if (std.mem.eql(u8, fmt, "j1")) {
res.content_type = .JSON; // reset to json res.content_type = .JSON; // reset to json
try Json.render(res.writer(), weather); try Json.render(res.writer(), weather);
} else if (std.mem.eql(u8, fmt, "p1")) { return;
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);
} }
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 { } else {
// No specific format selected, we'll provide Formatted output in either // No specific format selected, we'll provide Formatted output in either
// text (ansi/plain) or html // text (ansi/plain) or html

View file

@ -109,6 +109,27 @@ fn nowAt(coords: Coordinates) !i64 {
return new.unixTimestamp(); 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" { test "render custom format with location and temp" {
const allocator = std.testing.allocator; 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, "mph") != null);
try std.testing.expect(std.mem.indexOf(u8, output, "in") != 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);
}

View file

@ -3,170 +3,105 @@ const types = @import("../weather/types.zig");
const emoji = @import("emoji.zig"); const emoji = @import("emoji.zig");
const utils = @import("utils.zig"); const utils = @import("utils.zig");
pub fn render(writer: *std.Io.Writer, data: types.WeatherData, format: []const u8, use_imperial: bool) !void { const Format = enum(u3) {
if (std.mem.eql(u8, format, "1")) { @"1" = 1,
const temp = if (use_imperial) data.current.tempFahrenheit() else data.current.temp_c; @"2" = 2,
const unit = if (use_imperial) "°F" else "°C"; @"3" = 3,
try writer.print("{s}: {s} {d:.0}{s}", .{ @"4" = 4,
data.location, };
emoji.getWeatherEmoji(data.current.weather_code),
temp, pub fn render(writer: *std.Io.Writer, data: types.WeatherData, format: Format, use_imperial: bool) !void {
unit, switch (format) {
}); .@"1" => {
} else if (std.mem.eql(u8, format, "2")) { const temp = if (use_imperial) data.current.tempFahrenheit() else data.current.temp_c;
const temp = if (use_imperial) data.current.tempFahrenheit() else data.current.temp_c; const unit = if (use_imperial) "°F" else "°C";
const unit = if (use_imperial) "°F" else "°C"; try writer.print("{s}: {s} {d:.0}{s}", .{
const wind = if (use_imperial) data.current.windMph() else data.current.wind_kph; data.location,
const wind_unit = if (use_imperial) "mph" else "km/h"; emoji.getWeatherEmoji(data.current.weather_code),
try writer.print("{s}: {s} {d:.0}{s} {s}{s}{d:.0}{s}", .{ temp,
data.location, unit,
emoji.getWeatherEmoji(data.current.weather_code), });
temp, },
unit, .@"2" => {
"🌬️", const temp = if (use_imperial) data.current.tempFahrenheit() else data.current.temp_c;
utils.degreeToDirection(data.current.wind_deg), const unit = if (use_imperial) "°F" else "°C";
wind, const wind = if (use_imperial) data.current.windMph() else data.current.wind_kph;
wind_unit, const wind_unit = if (use_imperial) "mph" else "km/h";
}); try writer.print("{s}: {s} {d:.0}{s} {s}{s}{d:.0}{s}", .{
} else if (std.mem.eql(u8, format, "3")) { data.location,
const temp = if (use_imperial) data.current.tempFahrenheit() else data.current.temp_c; emoji.getWeatherEmoji(data.current.weather_code),
const unit = if (use_imperial) "°F" else "°C"; temp,
const wind = if (use_imperial) data.current.windMph() else data.current.wind_kph; unit,
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}%%", .{ utils.degreeToDirection(data.current.wind_deg),
data.location, wind,
emoji.getWeatherEmoji(data.current.weather_code), wind_unit,
temp, });
unit, },
"🌬️", .@"3" => {
utils.degreeToDirection(data.current.wind_deg), const temp = if (use_imperial) data.current.tempFahrenheit() else data.current.temp_c;
wind, const unit = if (use_imperial) "°F" else "°C";
wind_unit, const wind = if (use_imperial) data.current.windMph() else data.current.wind_kph;
"💧", const wind_unit = if (use_imperial) "mph" else "km/h";
data.current.humidity, try writer.print("{s}: {s} {d:.0}{s} {s}{s}{d:.0}{s} {s}{d}%%", .{
}); data.location,
} else if (std.mem.eql(u8, format, "4")) { emoji.getWeatherEmoji(data.current.weather_code),
const temp = if (use_imperial) data.current.tempFahrenheit() else data.current.temp_c; temp,
const unit = if (use_imperial) "°F" else "°C"; unit,
const wind = if (use_imperial) data.current.windMph() else data.current.wind_kph; "🌬️",
const wind_unit = if (use_imperial) "mph" else "km/h"; utils.degreeToDirection(data.current.wind_deg),
try writer.print("{s}: {s} {d:.0}{s} {s}{s}{d:.0}{s} {s}{d}%% {s}", .{ wind,
data.location, wind_unit,
emoji.getWeatherEmoji(data.current.weather_code), "💧",
temp, data.current.humidity,
unit, });
"🌬️", },
utils.degreeToDirection(data.current.wind_deg), .@"4" => {
wind, const temp = if (use_imperial) data.current.tempFahrenheit() else data.current.temp_c;
wind_unit, const unit = if (use_imperial) "°F" else "°C";
"💧", const wind = if (use_imperial) data.current.windMph() else data.current.wind_kph;
data.current.humidity, 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,
} else { emoji.getWeatherEmoji(data.current.weather_code),
try renderCustom(writer, data, format, use_imperial); 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 { const test_data = types.WeatherData{
var i: usize = 0; .location = "London",
while (i < format.len) { .coords = .{ .latitude = 0, .longitude = 0 },
if (format[i] == '%' and i + 1 < format.len) { .current = .{
const code = format[i + 1]; .temp_c = 15.0,
switch (code) { .feels_like_c = 15.0,
'c' => try writer.writeAll(emoji.getWeatherEmoji(data.current.weather_code)), .condition = "Clear",
'C' => try writer.writeAll(data.current.condition), .weather_code = .clear,
'h' => try writer.print("{d}", .{data.current.humidity}), .humidity = 65,
't' => { .wind_kph = 10.0,
const temp = if (use_imperial) data.current.tempFahrenheit() else data.current.temp_c; .wind_deg = 0.0,
try writer.print("{d:.0}", .{temp}); .pressure_mb = 1013.0,
}, .precip_mm = 0.0,
'f' => { .visibility_km = null,
const temp = if (use_imperial) data.current.tempFahrenheit() else data.current.temp_c; },
try writer.print("{d:.0}", .{temp}); .forecast = &.{},
}, .allocator = std.testing.allocator,
'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;
}
}
}
test "format 1" { 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 output_buf: [1024]u8 = undefined;
var writer = std.Io.Writer.fixed(&output_buf); var writer = std.Io.Writer.fixed(&output_buf);
try render(&writer, data, "1", false); try render(&writer, test_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);
const output = output_buf[0..writer.end]; const output = output_buf[0..writer.end];
@ -174,31 +109,56 @@ test "custom format" {
} }
test "format 2 with imperial units" { 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 output_buf: [1024]u8 = undefined;
var writer = std.Io.Writer.fixed(&output_buf); 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]; 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);
} }