wttr/src/location/Airports.zig

125 lines
4.4 KiB
Zig

const std = @import("std");
const Coordinates = @import("../Coordinates.zig");
pub const Airport = struct {
iata: []const u8,
name: []const u8,
coords: Coordinates,
};
const Airports = @This();
arena: std.heap.ArenaAllocator,
airports: std.StringHashMap(Airport),
pub fn init(allocator: std.mem.Allocator) !Airports {
const csv_data = @embedFile("airports.dat");
return try initFromData(allocator, csv_data);
}
pub fn initFromData(allocator: std.mem.Allocator, csv_data: []const u8) !Airports {
var arena = std.heap.ArenaAllocator.init(allocator);
const alloc = arena.allocator();
var airports = std.StringHashMap(Airport).init(alloc);
var lines = std.mem.splitScalar(u8, csv_data, '\n');
while (lines.next()) |line| {
if (line.len == 0) continue;
const airport = parseAirportLine(alloc, line) catch continue;
if (airport.iata.len == 3 and airports.get(airport.iata) == null) {
try airports.put(airport.iata, airport);
}
}
return Airports{
.arena = arena,
.airports = airports,
};
}
pub fn deinit(self: *Airports) void {
self.arena.deinit();
}
pub fn lookup(self: *Airports, iata_code: []const u8) ?Airport {
return self.airports.get(iata_code);
}
fn parseAirportLine(allocator: std.mem.Allocator, line: []const u8) !Airport {
// CSV format: ID,Name,City,Country,IATA,ICAO,Lat,Lon,...
var fields = std.mem.splitScalar(u8, line, ',');
_ = fields.next() orelse return error.InvalidFormat; // ID
const name_quoted = fields.next() orelse return error.InvalidFormat; // Name
_ = fields.next() orelse return error.InvalidFormat; // City
_ = fields.next() orelse return error.InvalidFormat; // Country
const iata_quoted = fields.next() orelse return error.InvalidFormat; // IATA
_ = fields.next() orelse return error.InvalidFormat; // ICAO
const lat_str = fields.next() orelse return error.InvalidFormat; // Lat
const lon_str = fields.next() orelse return error.InvalidFormat; // Lon
// Remove quotes from fields
const name = try unquote(allocator, name_quoted);
const iata = try unquote(allocator, iata_quoted);
// Skip if IATA is "\\N" (null)
if (std.mem.eql(u8, iata, "\\N")) {
allocator.free(name);
allocator.free(iata);
return error.NoIATA;
}
const lat = try std.fmt.parseFloat(f64, lat_str);
const lon = try std.fmt.parseFloat(f64, lon_str);
return Airport{
.iata = iata,
.name = name,
.coords = .{
.latitude = lat,
.longitude = lon,
},
};
}
fn unquote(allocator: std.mem.Allocator, quoted: []const u8) ![]const u8 {
if (quoted.len >= 2 and quoted[0] == '"' and quoted[quoted.len - 1] == '"') {
return allocator.dupe(u8, quoted[1 .. quoted.len - 1]);
}
return allocator.dupe(u8, quoted);
}
test "parseAirportLine valid" {
const allocator = std.testing.allocator;
const line = "1,\"Goroka Airport\",\"Goroka\",\"Papua New Guinea\",\"GKA\",\"AYGA\",-6.081689834590001,145.391998291,5282,10,\"U\",\"Pacific/Port_Moresby\",\"airport\",\"OurAirports\"";
const airport = try Airports.parseAirportLine(allocator, line);
defer allocator.free(airport.iata);
defer allocator.free(airport.name);
try std.testing.expectEqualStrings("GKA", airport.iata);
try std.testing.expectEqualStrings("Goroka Airport", airport.name);
try std.testing.expectApproxEqAbs(@as(f64, -6.081689834590001), airport.coords.latitude, 0.0001);
try std.testing.expectApproxEqAbs(@as(f64, 145.391998291), airport.coords.longitude, 0.0001);
}
test "parseAirportLine with null IATA" {
const allocator = std.testing.allocator;
const line = "1,\"Test Airport\",\"City\",\"Country\",\"\\N\",\"ICAO\",0.0,0.0";
try std.testing.expectError(error.NoIATA, Airports.parseAirportLine(allocator, line));
}
test "AirportDB lookup" {
const allocator = std.testing.allocator;
const csv = "1,\"Munich Airport\",\"Munich\",\"Germany\",\"MUC\",\"EDDM\",48.353802,11.7861,1487,1,\"E\",\"Europe/Berlin\",\"airport\",\"OurAirports\"";
var db = try Airports.initFromData(allocator, csv);
defer db.deinit();
const result = db.lookup("MUC");
try std.testing.expect(result != null);
try std.testing.expectEqualStrings("Munich Airport", result.?.name);
try std.testing.expectApproxEqAbs(@as(f64, 48.353802), result.?.coords.latitude, 0.0001);
}