mirror of
https://github.com/tiennm99/thptqg.git
synced 2026-10-05 00:13:44 +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.
112 lines
3.8 KiB
Go
112 lines
3.8 KiB
Go
// Package config loads per-dataset parse rules.
|
|
//
|
|
// The config deliberately carries no SQL. The table shape, the INSERT and the
|
|
// subject regexes are identical for every dataset and live in internal/schema;
|
|
// keeping them here means one copy of the DDL per dataset, which is how the
|
|
// 2016 and 2017 schemas drifted apart.
|
|
package config
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"os"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
// SheetMode selects which sheets of a workbook are read.
|
|
type SheetMode string
|
|
|
|
const (
|
|
// SheetModeAll iterates every sheet, which is what recovers the Hanoi and
|
|
// HCM rows that overflow past Excel's 65,536-row cap into a second sheet.
|
|
SheetModeAll SheetMode = "all"
|
|
// SheetModeFirst reads sheet 0 only.
|
|
SheetModeFirst SheetMode = "first"
|
|
)
|
|
|
|
// valid reports whether m is one of the two accepted sheet modes.
|
|
//
|
|
// Checked after decoding rather than via a custom unmarshaler: the decoder
|
|
// assigns named string types directly, so a typo would otherwise decode
|
|
// silently and be read as "not all" downstream.
|
|
func (m SheetMode) valid() bool {
|
|
return m == SheetModeAll || m == SheetModeFirst
|
|
}
|
|
|
|
// DatasetConfig is the per-dataset parse rule set.
|
|
type DatasetConfig struct {
|
|
Reader ReaderCfg `yaml:"reader"`
|
|
// Columns holds fixed column indices. Nil when FormatDetection handles
|
|
// per-file mapping — a pointer rather than a value because a zero ColumnMap
|
|
// would silently mean "every column is index 0".
|
|
Columns *ColumnMap `yaml:"columns"`
|
|
Validation ValidationCfg `yaml:"validation"`
|
|
Header HeaderCfg `yaml:"header"`
|
|
// FormatDetection, when set to "thptqg2016", enables per-file format
|
|
// auto-detection: each file's header row is inspected at runtime to choose
|
|
// the column layout (separate-scores / mapped / default-positional).
|
|
FormatDetection *string `yaml:"format_detection"`
|
|
}
|
|
|
|
type ReaderCfg struct {
|
|
SheetMode SheetMode `yaml:"sheet_mode"`
|
|
// StripBlankRows skips rows where every cell is empty before counting them
|
|
// as source rows.
|
|
StripBlankRows bool `yaml:"strip_blank_rows"`
|
|
}
|
|
|
|
// ColumnMap holds zero-indexed column positions in the source row. Used by the
|
|
// 2017 configs; 2016 uses runtime format detection instead.
|
|
type ColumnMap struct {
|
|
HoTen int `yaml:"ho_ten"`
|
|
NgaySinh int `yaml:"ngay_sinh"`
|
|
SoBaoDanh int `yaml:"so_bao_danh"`
|
|
DiemThi int `yaml:"diem_thi"`
|
|
}
|
|
|
|
type ValidationCfg struct {
|
|
// RequireNumericSbd skips rows whose so_bao_danh is not all ASCII digits.
|
|
RequireNumericSbd bool `yaml:"require_numeric_sbd"`
|
|
RequireNonemptyName bool `yaml:"require_nonempty_name"`
|
|
RequireNonemptySbd bool `yaml:"require_nonempty_sbd"`
|
|
}
|
|
|
|
type HeaderCfg struct {
|
|
// Tokens are matched against the uppercased first cell to detect a header row.
|
|
Tokens []string `yaml:"tokens"`
|
|
}
|
|
|
|
// Parse decodes a config, rejecting any key the struct does not declare.
|
|
//
|
|
// Strictness is load-bearing and has its own test. YAML decoders ignore unknown
|
|
// keys by default, so KnownFields(true) has to be set explicitly. Without it a
|
|
// leftover `schema:` mapping would look effective while internal/schema actually
|
|
// drove the build — the drift that produced two divergent schemas.
|
|
func Parse(src []byte) (*DatasetConfig, error) {
|
|
var cfg DatasetConfig
|
|
dec := yaml.NewDecoder(bytes.NewReader(src))
|
|
dec.KnownFields(true)
|
|
if err := dec.Decode(&cfg); err != nil {
|
|
return nil, fmt.Errorf("decode config: %w", err)
|
|
}
|
|
if !cfg.Reader.SheetMode.valid() {
|
|
return nil, fmt.Errorf("invalid sheet_mode %q (want %q or %q)",
|
|
cfg.Reader.SheetMode, SheetModeAll, SheetModeFirst)
|
|
}
|
|
return &cfg, nil
|
|
}
|
|
|
|
// Load reads and parses a config file.
|
|
func Load(path string) (*DatasetConfig, error) {
|
|
src, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read config %s: %w", path, err)
|
|
}
|
|
cfg, err := Parse(src)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%s: %w", path, err)
|
|
}
|
|
return cfg, nil
|
|
}
|