Files
tiennm99 933adf70c9 refactor: comments state current behavior, not project history
Comments across the tree justified the code by pointing at a Rust
implementation that is no longer in the repository, citing files and line
numbers (config.rs:132, schema.rs:26-54, reader.rs:42) that cannot be
opened, plus crates and datasets that are equally gone. A reader could not
check any of it.

Every invariant those comments carried is kept and restated so it stands on
its own: the bytewise sort that decides which row survives a duplicate exam
number, the literal U+0300..U+036F range that must match the site's toAscii,
the trailing space in "SINH ", the BIFF and shared-string corrections, the
VACUUM-after-COMMIT rule, the deploy-from-main guard.

The reader's contract is now anchored to the frozen oracle in
parser/testdata, which still exists and is still checked, rather than to the
tool that originally produced it.

TestDDLMatchesRust becomes TestDDLIsFrozen: it compares against a copy of
the DDL inside the test and never read schema.rs, so both the name and the
failure message were misleading.

ToAscii no longer claims the d-replacement must precede lowercasing. Both
cases map to 'd' and ToLower runs last, so the order has no effect.
2026-08-14 09:40:41 +07:00

115 lines
3.1 KiB
Go

// Command xlsxread reads .xls/.xlsx files and builds SQLite databases for the
// thptqg datasets.
//
// The CLI contract is fixed by the assembler, which drives this binary:
//
// xlsxread build --schema <config.yml> --input <dir> --output <db>
// xlsxread audit --schema <config.yml> --input <dir> --db <db>
//
// Implemented with the standard flag package rather than a CLI framework: two
// subcommands with three flags each do not justify a dependency.
package main
import (
"flag"
"fmt"
"os"
"github.com/tiennm99/thptqg/parser/internal/audit"
"github.com/tiennm99/thptqg/parser/internal/config"
"github.com/tiennm99/thptqg/parser/internal/ingest"
)
func usage() {
fmt.Fprint(os.Stderr, `xlsxread — read .xls/.xlsx files and build SQLite databases for thptqg datasets
Usage:
xlsxread build --schema <config.yml> --input <dir> --output <db>
xlsxread audit --schema <config.yml> --input <dir> --db <db>
`)
}
func main() {
if len(os.Args) < 2 {
usage()
os.Exit(2)
}
switch os.Args[1] {
case "build":
runBuild(os.Args[2:])
case "audit":
runAudit(os.Args[2:])
case "-h", "--help", "help":
usage()
default:
fmt.Fprintf(os.Stderr, "unknown subcommand %q\n\n", os.Args[1])
usage()
os.Exit(2)
}
}
func runBuild(args []string) {
fs := flag.NewFlagSet("build", flag.ExitOnError)
schemaPath := fs.String("schema", "", "path to the dataset YAML config file")
inputDir := fs.String("input", "", "directory containing the .xls / .xlsx source files")
outputPath := fs.String("output", "", "output SQLite database path")
fs.Parse(args)
if *schemaPath == "" || *inputDir == "" || *outputPath == "" {
fmt.Fprintln(os.Stderr, "build requires --schema, --input and --output")
os.Exit(2)
}
cfg, err := config.Load(*schemaPath)
if err != nil {
fatalf("Failed to load config: %v", err)
}
// The 2016 dataset selects its column layout per sheet at runtime; every
// other dataset uses the fixed columns: mapping from its config.
if cfg.FormatDetection != nil && *cfg.FormatDetection == "thptqg2016" {
if err := ingest.Detect2016(cfg, *inputDir, *outputPath); err != nil {
fatalf("%v", err)
}
return
}
if err := ingest.Standard(cfg, *inputDir, *outputPath); err != nil {
fatalf("%v", err)
}
}
func runAudit(args []string) {
fs := flag.NewFlagSet("audit", flag.ExitOnError)
schemaPath := fs.String("schema", "", "path to the dataset YAML config file")
inputDir := fs.String("input", "", "directory containing the .xlsx source files")
dbPath := fs.String("db", "", "SQLite database to compare against")
fs.Parse(args)
if *schemaPath == "" || *inputDir == "" || *dbPath == "" {
fmt.Fprintln(os.Stderr, "audit requires --schema, --input and --db")
os.Exit(2)
}
cfg, err := config.Load(*schemaPath)
if err != nil {
fatalf("Failed to load config: %v", err)
}
res, err := audit.Run(*inputDir, *dbPath, cfg)
if err != nil {
fatalf("%v", err)
}
audit.PrintReport(res)
// A mismatch is a non-zero exit even though the audit itself succeeded —
// CI treats it as a failure signal.
if !res.Matched {
os.Exit(1)
}
}
func fatalf(format string, a ...any) {
fmt.Fprintf(os.Stderr, "Error: "+format+"\n", a...)
os.Exit(1)
}