From bcab444bca0d13bccea1dc4a687e8e21e62106ce Mon Sep 17 00:00:00 2001 From: Emil Lerch Date: Thu, 18 Dec 2025 11:50:44 -0800 Subject: [PATCH] AI: add imperial/US default --- zig/API_ENDPOINTS.md | 10 ++++++++- zig/README.md | 1 + zig/src/http/handler.zig | 43 +++++++++++++++++++++++++++++++++++++- zig/src/location/geoip.zig | 42 +++++++++++++++++++++++++++++++++++++ zig/src/main.zig | 1 + 5 files changed, 95 insertions(+), 2 deletions(-) diff --git a/zig/API_ENDPOINTS.md b/zig/API_ENDPOINTS.md index 25bd0ec..8e79aea 100644 --- a/zig/API_ENDPOINTS.md +++ b/zig/API_ENDPOINTS.md @@ -157,7 +157,7 @@ curl wttr.in/London?Tnq |--------|-------------| | `A` | Force ANSI output (even for browsers) | | `n` | Narrow output (narrower terminal width) | -| `m` | Use metric units | +| `m` | Use metric units (force metric even for US) | | `M` | Use m/s for wind speed | | `u` | Use imperial units (Fahrenheit, mph) | | `I` | Inverted colors | @@ -172,6 +172,14 @@ curl wttr.in/London?Tnq | `Q` | Super quiet (no city name) | | `F` | No follow line (no "Follow @igor_chubin") | +**Unit System Defaults:** + +The service automatically selects units based on: +1. Explicit query parameter (`?u` or `?m`) - highest priority +2. Language parameter (`lang=us`) - forces imperial +3. Client IP geolocation - US IPs default to imperial +4. Default - metric for all other locations + **Examples:** ```bash curl wttr.in/London?T # Plain text, no ANSI diff --git a/zig/README.md b/zig/README.md index 9479b24..d1fe49c 100644 --- a/zig/README.md +++ b/zig/README.md @@ -15,6 +15,7 @@ This directory contains comprehensive documentation for rewriting wttr.in in Zig - Static help pages (/:help, /:translation) - Error handling (404/500 status codes) - Configuration from environment variables +- **Imperial units auto-detection**: Automatically uses imperial units (°F, mph) for US IP addresses and `lang=us`, with explicit `?u` and `?m` overrides ### Missing Features (To Be Implemented Later) diff --git a/zig/src/http/handler.zig b/zig/src/http/handler.zig index 3d7f2b4..9a9b0f0 100644 --- a/zig/src/http/handler.zig +++ b/zig/src/http/handler.zig @@ -16,6 +16,7 @@ pub const HandleWeatherOptions = struct { cache: *Cache, provider: WeatherProvider, resolver: *Resolver, + geoip: *@import("../location/geoip.zig").GeoIP, }; pub fn handleWeather( @@ -138,7 +139,25 @@ fn handleWeatherInternal( if (params.lang) |l| allocator.free(l); } - const use_imperial = if (params.units) |u| u == .uscs else false; + // Determine if imperial units should be used + // Priority: explicit ?u or ?m > lang=us > US IP > default metric + const use_imperial = blk: { + if (params.units) |u| { + break :blk u == .uscs; + } + if (params.lang) |lang| { + if (std.mem.eql(u8, lang, "us")) { + break :blk true; + } + } + const client_ip = getClientIP(req); + if (client_ip.len > 0) { + if (opts.geoip.isUSIP(client_ip)) { + break :blk true; + } + } + break :blk false; + }; const output = if (params.format) |fmt| blk: { if (std.mem.eql(u8, fmt, "j1")) { @@ -190,3 +209,25 @@ test "parseXForwardedFor trims whitespace" { test "parseXForwardedFor handles empty string" { try std.testing.expectEqualStrings("", parseXForwardedFor("")); } + +test "imperial units selection logic" { + // This test documents the priority order for unit selection: + // 1. Explicit ?u or ?m parameter (highest priority) + // 2. lang=us parameter + // 3. US IP detection + // 4. Default to metric + + // The actual logic is tested through integration tests + // This test just verifies the QueryParams parsing works + const allocator = std.testing.allocator; + + const params_u = try QueryParams.parse(allocator, "u"); + try std.testing.expectEqual(QueryParams.Units.uscs, params_u.units.?); + + const params_m = try QueryParams.parse(allocator, "m"); + try std.testing.expectEqual(QueryParams.Units.metric, params_m.units.?); + + const params_lang = try QueryParams.parse(allocator, "lang=us"); + defer allocator.free(params_lang.lang.?); + try std.testing.expectEqualStrings("us", params_lang.lang.?); +} diff --git a/zig/src/location/geoip.zig b/zig/src/location/geoip.zig index 60502a8..e658c53 100644 --- a/zig/src/location/geoip.zig +++ b/zig/src/location/geoip.zig @@ -114,6 +114,32 @@ pub const GeoIP = struct { return try self.extractCoordinates(result.entry); } + pub fn isUSIP(self: *GeoIP, ip: []const u8) bool { + const ip_z = std.heap.c_allocator.dupeZ(u8, ip) catch return false; + defer std.heap.c_allocator.free(ip_z); + + var gai_error: c_int = 0; + var mmdb_error: c_int = 0; + + const result = MMDB_lookup_string(&self.mmdb, ip_z.ptr, &gai_error, &mmdb_error); + + if (gai_error != 0 or mmdb_error != 0 or !result.found_entry) { + return false; + } + + var entry_mut = result.entry; + var country_data: MMDBEntryData = undefined; + const null_term: [*:0]const u8 = @ptrCast(&[_]u8{0}); + const status = MMDB_get_value(&entry_mut, &country_data, "country\x00", "iso_code\x00", null_term); + + if (status != 0 or !country_data.has_data) { + return false; + } + + const country_code = std.mem.span(country_data.utf8_string); + return std.mem.eql(u8, country_code, "US"); + } + fn extractCoordinates(self: *GeoIP, entry: MMDBEntry) !Coordinates { _ = self; var entry_mut = entry; @@ -143,3 +169,19 @@ test "GeoIP init with invalid path fails" { const result = GeoIP.init("/nonexistent/path.mmdb"); try std.testing.expectError(error.CannotOpenDatabase, result); } + +test "isUSIP detects US IPs" { + var geoip = GeoIP.init("./GeoLite2-City.mmdb") catch { + std.debug.print("Skipping test - GeoLite2-City.mmdb not found\n", .{}); + return error.SkipZigTest; + }; + defer geoip.deinit(); + + // Test that the function doesn't crash with various IPs + _ = geoip.isUSIP("8.8.8.8"); + _ = geoip.isUSIP("1.1.1.1"); + + // Test invalid IP returns false + const invalid = geoip.isUSIP("invalid"); + try std.testing.expect(!invalid); +} diff --git a/zig/src/main.zig b/zig/src/main.zig index 3b922bc..f51d381 100644 --- a/zig/src/main.zig +++ b/zig/src/main.zig @@ -91,6 +91,7 @@ pub fn main() !void { .cache = &cache, .provider = metno.provider(), .resolver = &resolver, + .geoip = &geoip, }, &rate_limiter); try server.listen();