mirror of
https://github.com/tiennm99/thptqg.git
synced 2026-08-14 07:20:59 +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.
118 lines
3.2 KiB
Go
118 lines
3.2 KiB
Go
package reader
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/pbnjay/grate"
|
|
// Registers the BIFF backend with grate.Open.
|
|
_ "github.com/pbnjay/grate/xls"
|
|
)
|
|
|
|
// xlsWorkbook reads legacy BIFF through pbnjay/grate.
|
|
//
|
|
// Do not swap grate back for extrame/xls: on this corpus it corrupted 69% of
|
|
// cells and dropped a further 28% — undecoded UTF-16LE and BIFF record framing
|
|
// leaked into cell values, content moved between rows and columns, and tail
|
|
// rows came back blank. Charset-independent, and unfixable from the outside.
|
|
//
|
|
// The one normalisation grate needs is trailing blank rows: it yields rows past
|
|
// the end of the reference used range (one for a populated sheet, two for an
|
|
// empty one), so trailing all-blank rows are trimmed. Note this is the opposite
|
|
// of the xlsx path, where excelize already trims and the reference keeps a 1x1
|
|
// empty range — in both cases the rule is "match the reference used range".
|
|
type xlsWorkbook struct {
|
|
sheets []Sheet
|
|
rows [][][]Cell
|
|
}
|
|
|
|
func openXLS(path string) (Workbook, error) {
|
|
wb, err := grate.Open(path)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("grate open %s: %w", path, err)
|
|
}
|
|
defer wb.Close()
|
|
|
|
names, err := wb.List()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("grate list %s: %w", path, err)
|
|
}
|
|
|
|
out := &xlsWorkbook{}
|
|
for idx, name := range names {
|
|
sh, err := wb.Get(name)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("grate get %s/%s: %w", path, name, err)
|
|
}
|
|
|
|
var raw [][]string
|
|
for sh.Next() {
|
|
row := sh.Strings()
|
|
cp := make([]string, len(row))
|
|
for i, v := range row {
|
|
cp[i] = demergeMarker(v)
|
|
}
|
|
raw = append(raw, cp)
|
|
}
|
|
|
|
// Trim to the reference used range.
|
|
height := len(raw)
|
|
for height > 0 && rowAllBlank(raw[height-1]) {
|
|
height--
|
|
}
|
|
raw = raw[:height]
|
|
|
|
width := 0
|
|
for _, r := range raw {
|
|
if len(r) > width {
|
|
width = len(r)
|
|
}
|
|
}
|
|
|
|
cells := make([][]Cell, len(raw))
|
|
for i, r := range raw {
|
|
row := make([]Cell, len(r))
|
|
for j, v := range r {
|
|
row[j] = Cell{Str: v, IsEmpty: v == ""}
|
|
}
|
|
cells[i] = padRow(row, width)
|
|
}
|
|
|
|
out.sheets = append(out.sheets, Sheet{Index: idx, Name: name, Height: height, Width: width})
|
|
out.rows = append(out.rows, cells)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// demergeMarker blanks grate's merged-cell continuation markers.
|
|
//
|
|
// grate fills the cells covered by a merge with sentinel runes; the reference
|
|
// reports them as empty. In this corpus they occur only in the merged title
|
|
// block of the 2016 spreadsheets (19 cells in rows 0-2 of one file). Only an
|
|
// exact whole-value match is blanked, so a real cell that merely contains an
|
|
// arrow is untouched.
|
|
func demergeMarker(v string) string {
|
|
switch v {
|
|
case grate.ContinueColumnMerged, grate.EndColumnMerged,
|
|
grate.ContinueRowMerged, grate.EndRowMerged:
|
|
return ""
|
|
}
|
|
return v
|
|
}
|
|
|
|
func (w *xlsWorkbook) Sheets() []Sheet { return w.sheets }
|
|
|
|
func (w *xlsWorkbook) EachRow(sheetIdx int, fn RowFunc) error {
|
|
if sheetIdx < 0 || sheetIdx >= len(w.sheets) {
|
|
return fmt.Errorf("sheet index %d out of range", sheetIdx)
|
|
}
|
|
sh := w.sheets[sheetIdx]
|
|
for i, row := range w.rows[sheetIdx] {
|
|
if err := fn(sh, i, row); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (w *xlsWorkbook) Close() error { return nil }
|