AI: Fix issues of imperial units in various renderers

This commit is contained in:
Emil Lerch 2025-12-18 11:41:37 -08:00
parent 58d09b1f7e
commit 4746ad81b9
Signed by: lobo
GPG key ID: A7B62D657EF764F8
6 changed files with 264 additions and 48 deletions

View file

@ -4,6 +4,7 @@ const Cache = @import("../cache/cache.zig").Cache;
const WeatherProvider = @import("../weather/provider.zig").WeatherProvider;
const Resolver = @import("../location/resolver.zig").Resolver;
const Location = @import("../location/resolver.zig").Location;
const QueryParams = @import("query.zig").QueryParams;
const ansi = @import("../render/ansi.zig");
const line = @import("../render/line.zig");
const json = @import("../render/json.zig");
@ -130,21 +131,27 @@ fn handleWeatherInternal(
};
defer weather.deinit();
const query = try req.query();
const format = query.get("format");
const query_string = req.url.query;
const params = try QueryParams.parse(allocator, query_string);
defer {
if (params.format) |f| allocator.free(f);
if (params.lang) |l| allocator.free(l);
}
const output = if (format) |fmt| blk: {
const use_imperial = if (params.units) |u| u == .uscs else false;
const output = if (params.format) |fmt| blk: {
if (std.mem.eql(u8, fmt, "j1")) {
res.content_type = .JSON;
break :blk try json.render(allocator, weather);
} else if (std.mem.eql(u8, fmt, "v2")) {
break :blk try v2.render(allocator, weather);
break :blk try v2.render(allocator, weather, use_imperial);
} else if (std.mem.startsWith(u8, fmt, "%")) {
break :blk try custom.render(allocator, weather, fmt);
break :blk try custom.render(allocator, weather, fmt, use_imperial);
} else {
break :blk try line.render(allocator, weather, fmt);
break :blk try line.render(allocator, weather, fmt, use_imperial);
}
} else try ansi.render(allocator, weather, .{});
} else try ansi.render(allocator, weather, .{ .use_imperial = use_imperial });
const ttl = 1000 + std.crypto.random.intRangeAtMost(u64, 0, 1000);
try opts.cache.put(cache_key, output, ttl);
@ -161,12 +168,11 @@ fn generateCacheKey(
location: ?[]const u8,
) ![]const u8 {
const loc = location orelse "";
const query = try req.query();
const format = query.get("format") orelse "";
const query_string = req.url.query;
return std.fmt.allocPrint(allocator, "{s}:{s}:{s}", .{
req.url.path,
loc,
format,
query_string,
});
}

View file

@ -59,6 +59,23 @@ test "parse format parameter" {
try std.testing.expectEqualStrings("j1", params.format.?);
}
test "parse units with question mark" {
const allocator = std.testing.allocator;
// Test with just "u" (no question mark in query string)
const params1 = try QueryParams.parse(allocator, "u");
try std.testing.expectEqual(QueryParams.Units.uscs, params1.units.?);
// Test with "u=" (empty value)
const params2 = try QueryParams.parse(allocator, "u=");
try std.testing.expectEqual(QueryParams.Units.uscs, params2.units.?);
// Test combined with other params
const params3 = try QueryParams.parse(allocator, "format=3&u");
defer if (params3.format) |f| allocator.free(f);
try std.testing.expectEqual(QueryParams.Units.uscs, params3.units.?);
}
test "parse units parameters" {
const allocator = std.testing.allocator;
const params_m = try QueryParams.parse(allocator, "m");
@ -66,6 +83,9 @@ test "parse units parameters" {
const params_u = try QueryParams.parse(allocator, "u");
try std.testing.expectEqual(QueryParams.Units.uscs, params_u.units.?);
const params_u_query = try QueryParams.parse(allocator, "u=");
try std.testing.expectEqual(QueryParams.Units.uscs, params_u_query.units.?);
}
test "parse multiple parameters" {

View file

@ -27,3 +27,27 @@ pub fn render(allocator: std.mem.Allocator, data: types.WeatherData, options: Re
return output.toOwnedSlice(allocator);
}
test "render with imperial units" {
const data = types.WeatherData{
.location = "Chicago",
.current = .{
.temp_c = 10.0,
.temp_f = 50.0,
.condition = "Clear",
.weather_code = 113,
.humidity = 60,
.wind_kph = 16.0,
.wind_dir = "N",
.pressure_mb = 1013.0,
.precip_mm = 0.0,
},
.forecast = &.{},
.allocator = std.testing.allocator,
};
const output = try render(std.testing.allocator, data, .{ .use_imperial = true });
defer std.testing.allocator.free(output);
try std.testing.expect(std.mem.indexOf(u8, output, "50.0°F") != null);
}

View file

@ -13,7 +13,7 @@ fn getWeatherIcon(code: u16) []const u8 {
return weather_icons[idx];
}
pub fn render(allocator: std.mem.Allocator, weather: types.WeatherData, format: []const u8) ![]const u8 {
pub fn render(allocator: std.mem.Allocator, weather: types.WeatherData, format: []const u8, use_imperial: bool) ![]const u8 {
var output: std.ArrayList(u8) = .empty;
errdefer output.deinit(allocator);
const writer = output.writer(allocator);
@ -26,12 +26,32 @@ pub fn render(allocator: std.mem.Allocator, weather: types.WeatherData, format:
'c' => try writer.print("{s}", .{getWeatherIcon(weather.current.weather_code)}),
'C' => try writer.print("{s}", .{weather.current.condition}),
'h' => try writer.print("{d}%", .{weather.current.humidity}),
't' => try writer.print("{d:.0}°C", .{weather.current.temp_c}),
'f' => try writer.print("{d:.0}°C", .{weather.current.temp_c}), // Feels like (same as temp for now)
'w' => try writer.print("{d:.0} km/h {s}", .{ weather.current.wind_kph, weather.current.wind_dir }),
't' => {
const temp = if (use_imperial) weather.current.temp_f else weather.current.temp_c;
const unit = if (use_imperial) "°F" else "°C";
try writer.print("{d:.0}{s}", .{ temp, unit });
},
'f' => {
const temp = if (use_imperial) weather.current.temp_f else weather.current.temp_c;
const unit = if (use_imperial) "°F" else "°C";
try writer.print("{d:.0}{s}", .{ temp, unit });
},
'w' => {
const wind = if (use_imperial) weather.current.wind_kph * 0.621371 else weather.current.wind_kph;
const unit = if (use_imperial) "mph" else "km/h";
try writer.print("{d:.0} {s} {s}", .{ wind, unit, weather.current.wind_dir });
},
'l' => try writer.print("{s}", .{weather.location}),
'p' => try writer.print("{d:.1} mm", .{weather.current.precip_mm}),
'P' => try writer.print("{d:.0} hPa", .{weather.current.pressure_mb}),
'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' => try writer.print("🌕", .{}), // Moon phase placeholder
'M' => try writer.print("15", .{}), // Moon day placeholder
'o' => try writer.print("0%", .{}), // Probability of precipitation placeholder
@ -76,7 +96,7 @@ test "render custom format with location and temp" {
.allocator = allocator,
};
const output = try render(allocator, weather, "%l: %c %t");
const output = try render(allocator, weather, "%l: %c %t", false);
defer allocator.free(output);
try std.testing.expect(std.mem.indexOf(u8, output, "London") != null);
@ -103,7 +123,7 @@ test "render custom format with newline" {
.allocator = allocator,
};
const output = try render(allocator, weather, "%l%n%C");
const output = try render(allocator, weather, "%l%n%C", false);
defer allocator.free(output);
try std.testing.expect(std.mem.indexOf(u8, output, "Paris\nClear") != null);
@ -129,9 +149,37 @@ test "render custom format with humidity and pressure" {
.allocator = allocator,
};
const output = try render(allocator, weather, "Humidity: %h, Pressure: %P");
const output = try render(allocator, weather, "Humidity: %h, Pressure: %P", false);
defer allocator.free(output);
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",
.current = .{
.temp_c = 10.0,
.temp_f = 50.0,
.condition = "Clear",
.weather_code = 113,
.humidity = 60,
.wind_kph = 16.0,
.wind_dir = "N",
.pressure_mb = 1013.0,
.precip_mm = 2.5,
},
.forecast = &[_]types.ForecastDay{},
.allocator = allocator,
};
const output = try render(allocator, weather, "%t %w %p", true);
defer allocator.free(output);
try std.testing.expect(std.mem.indexOf(u8, output, "50°F") != null);
try std.testing.expect(std.mem.indexOf(u8, output, "mph") != null);
try std.testing.expect(std.mem.indexOf(u8, output, "in") != null);
}

View file

@ -1,51 +1,72 @@
const std = @import("std");
const types = @import("../weather/types.zig");
pub fn render(allocator: std.mem.Allocator, data: types.WeatherData, format: []const u8) ![]const u8 {
pub fn render(allocator: std.mem.Allocator, data: types.WeatherData, format: []const u8, use_imperial: bool) ![]const u8 {
if (std.mem.eql(u8, format, "1")) {
return std.fmt.allocPrint(allocator, "{s}: {s} {d:.0}°C", .{
const temp = if (use_imperial) data.current.temp_f else data.current.temp_c;
const unit = if (use_imperial) "°F" else "°C";
return std.fmt.allocPrint(allocator, "{s}: {s} {d:.0}{s}", .{
data.location,
getConditionEmoji(data.current.weather_code),
data.current.temp_c,
temp,
unit,
});
} else if (std.mem.eql(u8, format, "2")) {
return std.fmt.allocPrint(allocator, "{s}: {s} {d:.0}°C {s}{s}{d:.0}km/h", .{
const temp = if (use_imperial) data.current.temp_f else data.current.temp_c;
const unit = if (use_imperial) "°F" else "°C";
const wind = if (use_imperial) data.current.wind_kph * 0.621371 else data.current.wind_kph;
const wind_unit = if (use_imperial) "mph" else "km/h";
return std.fmt.allocPrint(allocator, "{s}: {s} {d:.0}{s} {s}{s}{d:.0}{s}", .{
data.location,
getConditionEmoji(data.current.weather_code),
data.current.temp_c,
temp,
unit,
"🌬️",
data.current.wind_dir,
data.current.wind_kph,
wind,
wind_unit,
});
} else if (std.mem.eql(u8, format, "3")) {
return std.fmt.allocPrint(allocator, "{s}: {s} {d:.0}°C {s}{s}{d:.0}km/h {s}{d}%%", .{
const temp = if (use_imperial) data.current.temp_f else data.current.temp_c;
const unit = if (use_imperial) "°F" else "°C";
const wind = if (use_imperial) data.current.wind_kph * 0.621371 else data.current.wind_kph;
const wind_unit = if (use_imperial) "mph" else "km/h";
return std.fmt.allocPrint(allocator, "{s}: {s} {d:.0}{s} {s}{s}{d:.0}{s} {s}{d}%%", .{
data.location,
getConditionEmoji(data.current.weather_code),
data.current.temp_c,
temp,
unit,
"🌬️",
data.current.wind_dir,
data.current.wind_kph,
wind,
wind_unit,
"💧",
data.current.humidity,
});
} else if (std.mem.eql(u8, format, "4")) {
return std.fmt.allocPrint(allocator, "{s}: {s} {d:.0}°C {s}{s}{d:.0}km/h {s}{d}%% {s}", .{
const temp = if (use_imperial) data.current.temp_f else data.current.temp_c;
const unit = if (use_imperial) "°F" else "°C";
const wind = if (use_imperial) data.current.wind_kph * 0.621371 else data.current.wind_kph;
const wind_unit = if (use_imperial) "mph" else "km/h";
return std.fmt.allocPrint(allocator, "{s}: {s} {d:.0}{s} {s}{s}{d:.0}{s} {s}{d}%% {s}", .{
data.location,
getConditionEmoji(data.current.weather_code),
data.current.temp_c,
temp,
unit,
"🌬️",
data.current.wind_dir,
data.current.wind_kph,
wind,
wind_unit,
"💧",
data.current.humidity,
"☀️",
});
} else {
return renderCustom(allocator, data, format);
return renderCustom(allocator, data, format, use_imperial);
}
}
fn renderCustom(allocator: std.mem.Allocator, data: types.WeatherData, format: []const u8) ![]const u8 {
fn renderCustom(allocator: std.mem.Allocator, data: types.WeatherData, format: []const u8, use_imperial: bool) ![]const u8 {
var output: std.ArrayList(u8) = .empty;
errdefer output.deinit(allocator);
@ -57,12 +78,28 @@ fn renderCustom(allocator: std.mem.Allocator, data: types.WeatherData, format: [
'c' => try output.appendSlice(allocator, getConditionEmoji(data.current.weather_code)),
'C' => try output.appendSlice(allocator, data.current.condition),
'h' => try output.writer(allocator).print("{d}", .{data.current.humidity}),
't' => try output.writer(allocator).print("{d:.0}", .{data.current.temp_c}),
'f' => try output.writer(allocator).print("{d:.0}", .{data.current.temp_c}),
'w' => try output.writer(allocator).print("{s}{d:.0}km/h", .{ data.current.wind_dir, data.current.wind_kph }),
't' => {
const temp = if (use_imperial) data.current.temp_f else data.current.temp_c;
try output.writer(allocator).print("{d:.0}", .{temp});
},
'f' => {
const temp = if (use_imperial) data.current.temp_f else data.current.temp_c;
try output.writer(allocator).print("{d:.0}", .{temp});
},
'w' => {
const wind = if (use_imperial) data.current.wind_kph * 0.621371 else data.current.wind_kph;
const wind_unit = if (use_imperial) "mph" else "km/h";
try output.writer(allocator).print("{s}{d:.0}{s}", .{ data.current.wind_dir, wind, wind_unit });
},
'l' => try output.appendSlice(allocator, data.location),
'p' => try output.writer(allocator).print("{d:.1}", .{data.current.precip_mm}),
'P' => try output.writer(allocator).print("{d:.0}", .{data.current.pressure_mb}),
'p' => {
const precip = if (use_imperial) data.current.precip_mm * 0.0393701 else data.current.precip_mm;
try output.writer(allocator).print("{d:.1}", .{precip});
},
'P' => {
const pressure = if (use_imperial) data.current.pressure_mb * 0.02953 else data.current.pressure_mb;
try output.writer(allocator).print("{d:.0}", .{pressure});
},
'%' => try output.append(allocator, '%'),
else => {
try output.append(allocator, '%');
@ -112,7 +149,7 @@ test "format 1" {
.allocator = std.testing.allocator,
};
const output = try render(std.testing.allocator, data, "1");
const output = try render(std.testing.allocator, data, "1", false);
defer std.testing.allocator.free(output);
try std.testing.expectEqualStrings("London: ☀️ 15°C", output);
@ -136,8 +173,32 @@ test "custom format" {
.allocator = std.testing.allocator,
};
const output = try render(std.testing.allocator, data, "%l: %c %t°C");
const output = try render(std.testing.allocator, data, "%l: %c %t°C", false);
defer std.testing.allocator.free(output);
try std.testing.expectEqualStrings("London: ☀️ 15°C", output);
}
test "format 2 with imperial units" {
const data = types.WeatherData{
.location = "Portland",
.current = .{
.temp_c = 10.0,
.temp_f = 50.0,
.condition = "Cloudy",
.weather_code = 119,
.humidity = 70,
.wind_kph = 20.0,
.wind_dir = "SE",
.pressure_mb = 1013.0,
.precip_mm = 0.0,
},
.forecast = &.{},
.allocator = std.testing.allocator,
};
const output = try render(std.testing.allocator, data, "2", true);
defer std.testing.allocator.free(output);
try std.testing.expectEqualStrings("Portland: ☁️ 50°F 🌬SE12mph", output);
}

View file

@ -1,7 +1,7 @@
const std = @import("std");
const types = @import("../weather/types.zig");
pub fn render(allocator: std.mem.Allocator, weather: types.WeatherData) ![]const u8 {
pub fn render(allocator: std.mem.Allocator, weather: types.WeatherData, use_imperial: bool) ![]const u8 {
var output: std.ArrayList(u8) = .empty;
errdefer output.deinit(allocator);
const writer = output.writer(allocator);
@ -13,15 +13,34 @@ pub fn render(allocator: std.mem.Allocator, weather: types.WeatherData) ![]const
try writer.print(" Current conditions\n", .{});
try writer.print(" {s}\n", .{weather.current.condition});
try writer.writeAll(" 🌡️ ");
try writer.print("{d:.1}°C ({d:.1}°F)\n", .{ weather.current.temp_c, weather.current.temp_f });
if (use_imperial) {
try writer.print("{d:.1}°F\n", .{weather.current.temp_f});
} else {
try writer.print("{d:.1}°C ({d:.1}°F)\n", .{ weather.current.temp_c, weather.current.temp_f });
}
try writer.writeAll(" 💧 ");
try writer.print("{d}%\n", .{weather.current.humidity});
try writer.writeAll(" 🌬️ ");
try writer.print("{d:.1} km/h {s}\n", .{ weather.current.wind_kph, weather.current.wind_dir });
if (use_imperial) {
const wind_mph = weather.current.wind_kph * 0.621371;
try writer.print("{d:.1} mph {s}\n", .{ wind_mph, weather.current.wind_dir });
} else {
try writer.print("{d:.1} km/h {s}\n", .{ weather.current.wind_kph, weather.current.wind_dir });
}
try writer.writeAll(" 🔽 ");
try writer.print("{d:.1} hPa\n", .{weather.current.pressure_mb});
if (use_imperial) {
const pressure_inhg = weather.current.pressure_mb * 0.02953;
try writer.print("{d:.2} inHg\n", .{pressure_inhg});
} else {
try writer.print("{d:.1} hPa\n", .{weather.current.pressure_mb});
}
try writer.writeAll(" 💦 ");
try writer.print("{d:.1} mm\n\n", .{weather.current.precip_mm});
if (use_imperial) {
const precip_in = weather.current.precip_mm * 0.0393701;
try writer.print("{d:.2} in\n\n", .{precip_in});
} else {
try writer.print("{d:.1} mm\n\n", .{weather.current.precip_mm});
}
// Forecast
if (weather.forecast.len > 0) {
@ -29,9 +48,19 @@ pub fn render(allocator: std.mem.Allocator, weather: types.WeatherData) ![]const
for (weather.forecast) |day| {
try writer.print(" {s}: {s}\n", .{ day.date, day.condition });
try writer.writeAll("");
try writer.print("{d:.1}°C ", .{day.max_temp_c});
if (use_imperial) {
const max_f = day.max_temp_c * 9.0 / 5.0 + 32.0;
try writer.print("{d:.1}°F ", .{max_f});
} else {
try writer.print("{d:.1}°C ", .{day.max_temp_c});
}
try writer.writeAll("");
try writer.print("{d:.1}°C\n", .{day.min_temp_c});
if (use_imperial) {
const min_f = day.min_temp_c * 9.0 / 5.0 + 32.0;
try writer.print("{d:.1}°F\n", .{min_f});
} else {
try writer.print("{d:.1}°C\n", .{day.min_temp_c});
}
}
}
@ -58,7 +87,7 @@ test "render v2 format" {
.allocator = allocator,
};
const output = try render(allocator, weather);
const output = try render(allocator, weather, false);
defer allocator.free(output);
try std.testing.expect(output.len > 0);
@ -66,3 +95,31 @@ test "render v2 format" {
try std.testing.expect(std.mem.indexOf(u8, output, "Current conditions") != null);
try std.testing.expect(std.mem.indexOf(u8, output, "12.0°C") != null);
}
test "render v2 format with imperial units" {
const allocator = std.testing.allocator;
const weather = types.WeatherData{
.location = "Boston",
.current = .{
.temp_c = 10.0,
.temp_f = 50.0,
.condition = "Clear",
.weather_code = 113,
.humidity = 65,
.wind_kph = 16.0,
.wind_dir = "N",
.pressure_mb = 1013.0,
.precip_mm = 0.0,
},
.forecast = &[_]types.ForecastDay{},
.allocator = allocator,
};
const output = try render(allocator, weather, true);
defer allocator.free(output);
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, "inHg") != null);
}