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