mirror of
https://github.com/tiennm99/thptqg2017.git
synced 2026-09-02 08:21:11 +00:00
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.
115 lines
3.1 KiB
Go
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)
|
|
}
|