The WK1 reader also reuses one buffer for record payloads.
- commit
- 7fcd326e053e39bada37f305ed65f973fb046a30
- parent
- 2e9e581bcf9fc1cfa1befc7c70b16401b714cf45
- author
- Dami <dami.etoile@gmail.com>
| M | internal/ | 67 | ++++++++++++--------- |
| M | internal/ | 97 | +++++++++++++++++++----------- |
| M | internal/ | 61 | +++++++++++-------- |
| M | internal/ | 4 | +- |
@@ -36,6 +36,16 @@ func shown(s *sheet.Sheet, a sheet.Addr) string {
3636 return sheet.FormatText(s.Value(a), s.DisplayFormat(a))3737 }3838 39+// sameShown checks that every cell of src shows the same in got.40+func sameShown(t *testing.T, src, got *sheet.Sheet) {41+ t.Helper()42+ for _, a := range src.Addrs() {43+ if want, g := shown(src, a), shown(got, a); g != want {44+ t.Errorf("%s shows %q after the round trip, want %q", a, g, want)45+ }46+ }47+}48+3949 func input(s *sheet.Sheet, a sheet.Addr) string {4050 if c := s.Cell(a); c != nil {4151 return c.Input@@ -189,35 +199,34 @@ func TestDelimitedRoundTrip(t *testing.T) {
189199 })190200 src.SetFormat(sheet.NewRect(addr(t, "D2"), addr(t, "D3")), sheet.Preset(sheet.FmtPercent))191201 for _, k := range []Kind{CSV, TSV} {192- t.Run(k.String(), func(t *testing.T) {193- name := filepath.Join(t.TempDir(), "out"+k.Ext())194- res, err := Export(context.Background(), name, k, Snap(src, sheet.Rect{}, "out"), ExportOptions{})195- if err != nil {196- t.Fatal(err)197- }198- if res.Rows != 4 || len(res.Notes) != 1 || res.Notes[0] != "3 formulas saved as values" {199- t.Errorf("result %+v", res)200- }201- got, err := Import(context.Background(), name, Options{})202- if err != nil {203- t.Fatal(err)204- }205- if n := got.Sheet.Name(); n != "out" || got.Sheet.CanUndo() {206- t.Errorf("imported sheet named %q, undoable %v", n, got.Sheet.CanUndo())207- }208- for _, a := range src.Addrs() {209- if want, g := shown(src, a), shown(got.Sheet, a); g != want {210- t.Errorf("%s shows %q after the round trip, want %q", a, g, want)211- }212- }213- // Values come back as numbers with their formats, not text.214- if v := got.Sheet.Value(addr(t, "C4")); v.Kind != sheet.Number || v.Num != 2062.4 {215- t.Errorf("C4 = %+v", v)216- }217- if f := got.Sheet.DisplayFormat(addr(t, "D2")); f.Kind != sheet.FmtPercent {218- t.Errorf("D2 format = %+v", f)219- }220- })202+ t.Run(k.String(), func(t *testing.T) { roundTripDelimited(t, src, k) })203+ }204+}205+206+// roundTripDelimited exports src as k, imports it back and compares.207+func roundTripDelimited(t *testing.T, src *sheet.Sheet, k Kind) {208+ name := filepath.Join(t.TempDir(), "out"+k.Ext())209+ res, err := Export(context.Background(), name, k, Snap(src, sheet.Rect{}, "out"), ExportOptions{})210+ if err != nil {211+ t.Fatal(err)212+ }213+ if res.Rows != 4 || len(res.Notes) != 1 || res.Notes[0] != "3 formulas saved as values" {214+ t.Errorf("result %+v", res)215+ }216+ got, err := Import(context.Background(), name, Options{})217+ if err != nil {218+ t.Fatal(err)219+ }220+ if n := got.Sheet.Name(); n != "out" || got.Sheet.CanUndo() {221+ t.Errorf("imported sheet named %q, undoable %v", n, got.Sheet.CanUndo())222+ }223+ sameShown(t, src, got.Sheet)224+ // Values come back as numbers with their formats, not text.225+ if v := got.Sheet.Value(addr(t, "C4")); v.Kind != sheet.Number || v.Num != 2062.4 {226+ t.Errorf("C4 = %+v", v)227+ }228+ if f := got.Sheet.DisplayFormat(addr(t, "D2")); f.Kind != sheet.FmtPercent {229+ t.Errorf("D2 format = %+v", f)221230 }222231 }223232 @@ -75,33 +75,19 @@ type wk1Reader struct {
7575 style map[sheet.Addr]sheet.Style7676 names int7777 maxRow int78+ buf []byte // the payload of the record being read7879 }7980 8081 // readWK1 reads a worksheet record by record. progress gets the number8182 // of cells read so far.8283 func readWK1(ctx context.Context, r io.Reader, progress func(cells int)) (*sheet.Sheet, []string, int, error) {8384 w := &wk1Reader{b: newBuilder(), version: wk1File}84- var head [4]byte8585 cells := 08686 for i := 0; ; i++ {87- if _, err := io.ReadFull(r, head[:]); err != nil {88- if i == 0 {89- return nil, nil, 0, errors.New("not a Lotus 1-2-3 worksheet: the file is empty")90- }91- if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) {92- break // no EOF record; keep what was read, as 1-2-3 does93- }87+ op, data, err := w.next(r, i)88+ if err != nil {9489 return nil, nil, 0, err9590 }96- op := binary.LittleEndian.Uint16(head[0:])97- n := binary.LittleEndian.Uint16(head[2:])98- if i == 0 && (op != wkBOF || n != 2) {99- return nil, nil, 0, errors.New("not a Lotus 1-2-3 worksheet: it doesn't start with a BOF record")100- }101- data := make([]byte, n)102- if _, err := io.ReadFull(r, data); err != nil {103- return nil, nil, 0, fmt.Errorf("record %d (opcode 0x%02X) is cut off", i+1, op)104- }10591 if op == wkEOF {10692 break10793 }@@ -124,6 +110,35 @@ func readWK1(ctx context.Context, r io.Reader, progress func(cells int)) (*sheet
124110 return s, notes, w.maxRow + 1, nil125111 }126112 113+// next reads record i, returning its opcode and payload, which is only114+// valid until the next call. The end of the file reads as an EOF record:115+// a worksheet may lack one, and 1-2-3 keeps what was read.116+func (w *wk1Reader) next(r io.Reader, i int) (uint16, []byte, error) {117+ var head [4]byte118+ if _, err := io.ReadFull(r, head[:]); err != nil {119+ switch {120+ case i == 0:121+ return 0, nil, errors.New("not a Lotus 1-2-3 worksheet: the file is empty")122+ case errors.Is(err, io.EOF), errors.Is(err, io.ErrUnexpectedEOF):123+ return wkEOF, nil, nil124+ }125+ return 0, nil, err126+ }127+ op := binary.LittleEndian.Uint16(head[0:])128+ n := int(binary.LittleEndian.Uint16(head[2:]))129+ if i == 0 && (op != wkBOF || n != 2) {130+ return 0, nil, errors.New("not a Lotus 1-2-3 worksheet: it doesn't start with a BOF record")131+ }132+ if cap(w.buf) < n {133+ w.buf = make([]byte, n)134+ }135+ data := w.buf[:n]136+ if _, err := io.ReadFull(r, data); err != nil {137+ return 0, nil, fmt.Errorf("record %d (opcode 0x%02X) is cut off", i+1, op)138+ }139+ return op, data, nil140+}141+127142 // record handles one record, reporting whether it was a cell.128143 func (w *wk1Reader) record(op uint16, d []byte) bool {129144 switch op {@@ -172,26 +187,7 @@ func (w *wk1Reader) record(op uint16, d []byte) bool {
172187 case wkLabel:173188 w.label(a, body)174189 case wkFormula:175- if len(body) < 10 {176- return false177- }178- v := math.Float64frombits(binary.LittleEndian.Uint64(body))179- size := int(binary.LittleEndian.Uint16(body[8:]))180- code := body[10:]181- if size < len(code) {182- code = code[:size]183- }184- w.hasString = false185- text, ok := lotusFormula(code, a, w.version)186- keep := func() {187- w.b.number(a, fromExcelSerial(v, f), f, sheet.Style{})188- w.pending, w.hasString = a, true189- }190- if !ok {191- w.b.kept(a, text, keep)192- return true193- }194- w.b.formula(a, "="+text, f, sheet.Style{}, keep)190+ return w.formula(a, f, body)195191 case wkString:196192 if w.hasString && w.pending == a {197193 w.b.text(a, lics(cstring(body)), sheet.Format{}, sheet.Style{})@@ -202,6 +198,33 @@ func (w *wk1Reader) record(op uint16, d []byte) bool {
202198 return true203199 }204200 201+// formula stores a formula: its cached value, the bytecode's size and202+// the bytecode. A formula that can't be translated keeps its value, or203+// the text in the STRING record that may follow.204+func (w *wk1Reader) formula(a sheet.Addr, f sheet.Format, body []byte) bool {205+ if len(body) < 10 {206+ return false207+ }208+ v := math.Float64frombits(binary.LittleEndian.Uint64(body))209+ size := int(binary.LittleEndian.Uint16(body[8:]))210+ code := body[10:]211+ if size < len(code) {212+ code = code[:size]213+ }214+ w.hasString = false215+ text, ok := lotusFormula(code, a, w.version)216+ keep := func() {217+ w.b.number(a, fromExcelSerial(v, f), f, sheet.Style{})218+ w.pending, w.hasString = a, true219+ }220+ if !ok {221+ w.b.kept(a, text, keep)222+ return true223+ }224+ w.b.formula(a, "="+text, f, sheet.Style{}, keep)225+ return true226+}227+205228 // label stores a label, whose first character sets its alignment: '206229 // left, " right, ^ centered, \ repeated to fill the cell.207230 func (w *wk1Reader) label(a sheet.Addr, body []byte) {@@ -314,39 +314,16 @@ func (w *xlsxWriter) sheet(ws string, snap *Snapshot) (int, error) {
314314 }315315 }316316 r := snap.Range317+ line := make([]any, r.To.Col-r.From.Col+1)317318 for row := r.From.Row; row <= r.To.Row; row++ {318- var line []any319319 for col := r.From.Col; col <= r.To.Col; col++ {320320 a := sheet.Addr{Col: col, Row: row}321- c, ok := snap.Cells[a]322- if !ok {323- line = append(line, nil)324- continue325- }326- // A formula's inferred format is written too, so its result327- // shows the same in Excel.328- f := c.Format329- id := 0330- if !f.IsZero() || !c.Style.IsZero() {331- if id, err = w.styleFor(f, c.Style); err != nil {321+ line[col-r.From.Col] = nil322+ if c, ok := snap.Cells[a]; ok {323+ if line[col-r.From.Col], err = w.cell(ws, a, c); err != nil {332324 return 0, err333325 }334326 }335- xc := excelize.Cell{StyleID: id, Value: xlsxValue(c.Value, f)}336- if c.Formula {337- if fx, ok := toExcelFormula(c.Input); ok {338- xc.Formula = fx339- } else {340- w.values++341- if w.example == "" {342- w.example = a.String()343- if w.multi {344- w.example = sheet.QuoteSheet(ws) + "!" + a.String()345- }346- }347- }348- }349- line = append(line, xc)350327 }351328 cell, _ := excelize.CoordinatesToCellName(r.From.Col+1, row+1)352329 if err := sw.SetRow(cell, line); err != nil {@@ -359,6 +336,36 @@ func (w *xlsxWriter) sheet(ws string, snap *Snapshot) (int, error) {
359336 return r.To.Row - r.From.Row + 1, nil360337 }361338 339+// cell is the snapshot cell c at a as excelize writes it. A formula with340+// no Excel equivalent is written as its value, and counted.341+func (w *xlsxWriter) cell(ws string, a sheet.Addr, c SnapCell) (excelize.Cell, error) {342+ // A formula's inferred format is written too, so its result shows343+ // the same in Excel.344+ id := 0345+ if !c.Format.IsZero() || !c.Style.IsZero() {346+ var err error347+ if id, err = w.styleFor(c.Format, c.Style); err != nil {348+ return excelize.Cell{}, err349+ }350+ }351+ xc := excelize.Cell{StyleID: id, Value: xlsxValue(c.Value, c.Format)}352+ if !c.Formula {353+ return xc, nil354+ }355+ if fx, ok := toExcelFormula(c.Input); ok {356+ xc.Formula = fx357+ return xc, nil358+ }359+ w.values++360+ if w.example == "" {361+ w.example = a.String()362+ if w.multi {363+ w.example = sheet.QuoteSheet(ws) + "!" + a.String()364+ }365+ }366+ return xc, nil367+}368+362369 // xlsxValue is a computed value as excelize writes it.363370 func xlsxValue(v sheet.Value, f sheet.Format) any {364371 switch v.Kind {@@ -62,10 +62,8 @@ func TestXLSXRoundTrip(t *testing.T) {
6262 t.Fatal(err)6363 }6464 s := got.Sheet65+ sameShown(t, src, s)6566 for _, a := range src.Addrs() {66- if want, g := shown(src, a), shown(s, a); g != want {67- t.Errorf("%s shows %q after the round trip, want %q", a, g, want)68- }6967 if want, g := src.Cell(a).Style, s.Cell(a).Style; g != want {7068 t.Errorf("%s style %+v, want %+v", a, g, want)7169 }