AI: add imperial/US default

This commit is contained in:
Emil Lerch 2025-12-18 11:50:44 -08:00
parent 4746ad81b9
commit bcab444bca
Signed by: lobo
GPG key ID: A7B62D657EF764F8
5 changed files with 95 additions and 2 deletions

View file

@ -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

View file

@ -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)

View file

@ -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.?);
}

View file

@ -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);
}

View file

@ -91,6 +91,7 @@ pub fn main() !void {
.cache = &cache,
.provider = metno.provider(),
.resolver = &resolver,
.geoip = &geoip,
}, &rate_limiter);
try server.listen();