mirror of
https://github.com/tiennm99/thptqg2017.git
synced 2026-09-02 14: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.
157 lines
4.2 KiB
Go
157 lines
4.2 KiB
Go
package reader
|
|
|
|
import (
|
|
"archive/zip"
|
|
"bytes"
|
|
"encoding/xml"
|
|
"io"
|
|
"strconv"
|
|
|
|
"github.com/xuri/excelize/v2"
|
|
)
|
|
|
|
// normalizeNumeric reproduces the reference rendering of a numeric cell.
|
|
//
|
|
// The reference parses a numeric cell to a float and prints its shortest
|
|
// round-tripping form, so the stored literal "6.0" becomes "6". RawCellValue
|
|
// hands back the literal instead.
|
|
//
|
|
// The cell type must be consulted, not guessed: "01063476", "6.00" and "NAN" are
|
|
// all shared strings that survive ParseFloat, and renumbering them would drop a
|
|
// leading zero, drop a trailing zero, or recase NaN. GetCellType is only called
|
|
// when re-rendering would actually change the text, which keeps it off the hot
|
|
// path for the ~99% of cells that are already canonical or plainly non-numeric.
|
|
func normalizeNumeric(f *excelize.File, sheet string, col, row int, v string) string {
|
|
if v == "" {
|
|
return v
|
|
}
|
|
fv, err := strconv.ParseFloat(v, 64)
|
|
if err != nil {
|
|
return v
|
|
}
|
|
out := strconv.FormatFloat(fv, 'f', -1, 64)
|
|
if out == v {
|
|
return v
|
|
}
|
|
axis, err := excelize.CoordinatesToCellName(col+1, row+1)
|
|
if err != nil {
|
|
return v
|
|
}
|
|
// OOXML omits the t attribute on numeric cells, and excelize has no map
|
|
// entry for an empty t, so a plain number reports CellTypeUnset rather than
|
|
// CellTypeNumber. Unset is only reachable here for a cell that exists and
|
|
// parsed as a float — an absent cell is "" and returned above — so both
|
|
// values mean "numeric". Shared strings report CellTypeSharedString and are
|
|
// left alone, which is what protects "01063476", "6.00" and "NAN".
|
|
if t, err := f.GetCellType(sheet, axis); err == nil &&
|
|
(t == excelize.CellTypeNumber || t == excelize.CellTypeUnset) {
|
|
return out
|
|
}
|
|
return v
|
|
}
|
|
|
|
// buildCRFixups maps a shared string's line-ending-normalised form back to its
|
|
// raw form, for the strings that contain a carriage return.
|
|
//
|
|
// Go's encoding/xml performs the line-ending normalisation the XML 1.0 spec
|
|
// mandates (CRLF and lone CR both become LF), so excelize returns "a\nb" where
|
|
// the reference — which reads the raw bytes — returns "a\r\nb". That difference
|
|
// reaches the database: in one 2016 file it affects 2,233 TEN_CUMTHI values,
|
|
// which populate the ten_cum_thi column.
|
|
//
|
|
// The trick is that character references are exempt from that normalisation, so
|
|
// rewriting literal CR bytes to before decoding round-trips them intact.
|
|
//
|
|
// Returns nil when the file has no CR at all, which is the common case.
|
|
func buildCRFixups(path string) map[string]string {
|
|
zr, err := zip.OpenReader(path)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
defer zr.Close()
|
|
|
|
var entry *zip.File
|
|
for _, zf := range zr.File {
|
|
if zf.Name == "xl/sharedStrings.xml" {
|
|
entry = zf
|
|
break
|
|
}
|
|
}
|
|
if entry == nil {
|
|
return nil
|
|
}
|
|
rc, err := entry.Open()
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
defer rc.Close()
|
|
data, err := io.ReadAll(rc)
|
|
if err != nil || !bytes.ContainsRune(data, '\r') {
|
|
return nil
|
|
}
|
|
data = bytes.ReplaceAll(data, []byte{'\r'}, []byte(" "))
|
|
|
|
fixups := make(map[string]string)
|
|
dec := xml.NewDecoder(bytes.NewReader(data))
|
|
var cur bytes.Buffer
|
|
inSI, inT := false, false
|
|
for {
|
|
tok, err := dec.Token()
|
|
if err != nil {
|
|
break
|
|
}
|
|
switch t := tok.(type) {
|
|
case xml.StartElement:
|
|
switch t.Name.Local {
|
|
case "si":
|
|
inSI, cur = true, bytes.Buffer{}
|
|
case "t":
|
|
inT = true
|
|
}
|
|
case xml.CharData:
|
|
if inSI && inT {
|
|
cur.Write(t)
|
|
}
|
|
case xml.EndElement:
|
|
switch t.Name.Local {
|
|
case "t":
|
|
inT = false
|
|
case "si":
|
|
if inSI {
|
|
raw := cur.String()
|
|
if norm := normalizeEOL(raw); norm != raw {
|
|
fixups[norm] = raw
|
|
}
|
|
}
|
|
inSI = false
|
|
}
|
|
}
|
|
}
|
|
if len(fixups) == 0 {
|
|
return nil
|
|
}
|
|
return fixups
|
|
}
|
|
|
|
// normalizeEOL applies XML 1.0 line-ending normalisation: CRLF and lone CR
|
|
// both collapse to LF. This is what encoding/xml does to the raw bytes, so it
|
|
// reproduces the text excelize hands back.
|
|
func normalizeEOL(s string) string {
|
|
if !bytes.ContainsRune([]byte(s), '\r') {
|
|
return s
|
|
}
|
|
var b bytes.Buffer
|
|
b.Grow(len(s))
|
|
for i := 0; i < len(s); i++ {
|
|
if s[i] == '\r' {
|
|
if i+1 < len(s) && s[i+1] == '\n' {
|
|
continue // CRLF: the LF is emitted on the next pass
|
|
}
|
|
b.WriteByte('\n') // lone CR
|
|
continue
|
|
}
|
|
b.WriteByte(s[i])
|
|
}
|
|
return b.String()
|
|
}
|