Skip to content
2024

flight-data-prettifier

A command-line tool that turns raw flight logs into readable text.

Overview

I built a Go command-line tool that turns marked airport codes and date/time tokens into readable flight notes. It separates reference-data loading from text rewriting and uses the standard library throughout.

context
Coursework
status
Archived learning project
role
Solo
started
Mar 2024
setting
Command-line text-processing exercise using flight notes and an airport-reference CSV.
built with
  • Go

Key decisions

Find reference columns by name
The airport loader resolves CSV header names rather than fixed column positions, so ordinary column reordering does not change the intended lookup.
Build each rewritten result from spans
Each conversion copies unchanged spans into strings.Builder and replaces recognised regions, avoiding repeated concatenation of the full output.

Demonstrated results

IATA and ICAO lookups have separate keys
The lookup stores airport references with distinct single- and double-hash prefixes, allowing the converter to distinguish the two code formats.
Date and time tokens use standard parsing
The conversion code uses Go's time package for the supported date, 12-hour and 24-hour forms, preserving malformed date tokens when parsing fails.

Known limitations

Failure paths report a successful exit status
Malformed lookup data can terminate the tool with status zero, so a calling shell or CI job cannot reliably recognise that failure.
Splitting lines is not complete CSV parsing
The loader uses strings.Split, which mishandles quoted commas. A malformed row also aborts the lookup rather than following a skip-and-report policy.

Zero dependencies, verifiably

./flight-data-prettifier ./input.txt ./output.md ./airport-lookup.csv

568 lines of Go, four files, and no third-party dependencies at all.

go.mod declares no external requirements, and the source imports only packages from the Go standard library and this project's own module. Those are the checks behind the dependency claim; absence of go.sum alone would not establish it. The standard-library imports are:

bufio · flag · fmt · os · regexp · strings · time

Every capability is standard library: the scanner, the regexp engine, the builder, the argument parsing. For a text-processing tool over one CSV, that is the right call. There was no library-shaped problem to solve.

Two keys per row

The loader indexes columns by name, not position, and bails if any is missing:

nameIndex := findColumnIndex(header, "name")
isoCountryIndex := findColumnIndex(header, "iso_country")
cityIndex := findColumnIndex(header, "municipality")
icaoCodeIndex := findColumnIndex(header, "icao_code")
iataCodeIndex := findColumnIndex(header, "iata_code")
coordinatesIndex := findColumnIndex(header, "coordinates")

if nameIndex == -1 || icaoCodeIndex == -1 || ... {
    fmt.Println("Airport lookup malformed")
    os.Exit(0)
}

That is column-order-independent, which is worth more than it looks. Reordering the CSV would break a positional reader.

Each row then contributes two map keys:

airportLookup["##"+icaoCode] = []string{name, city}
airportLookup["#"+iataCode]  = []string{name, city}

4,085 rows gives up to 8,170 entries. The ## / # prefix split is what lets a later pass rewrite codes while distinguishing them from ordinary words: any bare #FOO identifies the IATA form, and ##ABCD the ICAO form. The count is an upper bound rather than exact. A handful of small airports share an ICAO code, so real collisions silently overwrite each other.

One pass, no concatenation

The rewrite is a single left-to-right scan. Untouched spans are copied wholesale and only matched regions are rewritten:

builder.WriteString(text[lastIndex:start])   // verbatim span before the match
// ... rewrite the match ...
builder.WriteString(text[lastIndex:])        // remainder

The airport and date/time converters each build their result left-to-right. Using strings.Builder avoids repeatedly copying the entire accumulated output; that is an implementation choice, not a measured end-to-end speedup.

Date and time conversion

ConvertDatesAndTimes recognises D(...), T12(...) and T24(...) tokens. It parses the supported timestamp forms with Go's time package and formats the result for a date or a 12-/24-hour clock. A date token that fails parsing is copied back unchanged. This is separate from the airport-reference loader's file-level failure behaviour below.

Three ways it fails silently

The three failure paths all print a message and exit zero:

if !scanner.Scan() {
    fmt.Println("Airport lookup is empty")
    os.Exit(0)
}
//If any column contains blank data (empty cell), the data is considered to be malformed
if len(fields) < 6 || fields[nameIndex] == "" || ... {
    fmt.Println("Airport lookup malformed")
    os.Exit(0)
}

Exit status 0 means success to every shell, Makefile and CI runner. A pipeline like ./flight-data-prettifier in.txt out.md lookup.csv && publish would publish the previous run's output on a malformed input, and nothing would report an error. These should be os.Exit(1).

The row check also runs inside the scan loop, so one bad row three thousand rows in discards the entire lookup that came before it. The tool does not control its input. An airport list downloaded from somewhere is a realistic failure, and the response is total abandonment rather than skipping the row and reporting it.

The CSV reader is not a CSV reader

fields := strings.Split(scanner.Text(), ",")

encoding/csv exists in the standard library and handles quoted fields, embedded commas and doubled quotes. strings.Split handles none of them. Airport names routinely contain commas, city names, county names, "Washington, D.C.", and a quoted field containing one shifts every column to the right of it. The fields[nameIndex] == "" check then either fires on a shifted empty cell, or passes with the wrong value under the wrong name.

The tool uses the coordinates column only for validation and never parses it, which limits the damage. The loader is still reading a CSV with a non-CSV reader, and the fix is one import.

Scope of the exercise

This is a focused text-processing exercise: argument handling, reference-data lookup and token rewriting. Its separation of loading and conversion is useful, while correct CSV parsing and failure exit codes remain concrete improvements for the archived implementation.

Stack and source figures

how it fits together

rewriting
  • strings.Builder over one pass Untouched spans are copied wholesale and only matched regions rewritten. Never repeated concatenation of the full output. This describes the construction strategy, not a benchmark of the complete conversion pipeline.
parsing
  • bufio.Scanner + header-name lookup Columns indexed by name rather than position, so reordering the CSV does not break it. Quoted fields containing commas do break it, because the reader is strings.Split.
dependencies
  • Standard library only go.mod declares no external requirements, and the source imports only standard-library and project-local packages.
LOC
568 Across four Go files. No external requirements in go.mod; imports are standard-library or project-local.
airports
4,085 Data rows in airport-lookup.csv. The earlier count of 4,086 included the header.
lookup keys
up to 8,170 Two per row: `#IATA` and `##ICAO`. Actual count is lower where codes collide.
commits
37 2024-03-28 to 2026-06-14.
Full issue record · 3 entries

what broke, and what happened

  • 3 unresolved / 3

Every entry below says what broke and what became of it. An unresolved one is listed as unresolved rather than dropped.

  1. unresolved

    Malformed input exits with status 0

    It calls `fmt.Println("Airport lookup malformed"); os.Exit(0)` in three places: empty file, missing columns, and any short row. A shell pipeline or CI step reads that as success.

    then: Not fixed. Four `os.Exit(0)` calls remain in `airport_lookup.go`, so a malformed airport list still reports success to whatever invoked it.

  2. unresolved

    The CSV reader is strings.Split, not encoding/csv

    `strings.Split(scanner.Text(), ",")` breaks on any quoted field containing a comma, which airport names regularly do.

    then: Unresolved. Both call sites still split on a bare comma, so a quoted name containing one shifts every column after it.

  3. unresolved

    One bad row aborts the whole file

    The row check calls os.Exit(0) from inside the scan loop, so a single malformed row 3,000 rows in throws away the entire lookup.

    then: Same cause as the exit-code entry, not a separate defect. Still no skip-and-warn path, so one bad row costs the whole lookup.