A formula or number formatted as Plain text afterwards kept computing but reopened as text, and text typed into a Plain text cell reopened as a number or formula once the format changed (or failed to open when it didn't parse). A cell's text field says how its input reads when its format would say otherwise.
- commit
- abb0201c69eda4bb32ff08c4c21f7ffb1534f6ec
- parent
- cecd28e78723b3871966f2aca2ddc3e3289c4b79
- author
- Dami <dami.etoile@gmail.com>
- kind
- feat risk score 1.9 of 3, Jev's estimate
| M | docs/ | 1 | + |
| M | internal/ | 58 | +++++++++++++++++++++++++----- |
| M | internal/ | 27 | ++++++++++++++ |
| M | internal/ | 14 | +++---- |
| M | internal/ | 6 | ++-- |
| M | internal/ | 25 | +++++++++++-- |
| M | internal/ | 2 | +- |
@@ -101,6 +101,7 @@ save), so they raise no version:
101101 |---|---|---|102102 | `note` | a cell | Its [note](../sheets/notes-protection.md#notes) |103103 | `own` | a cell | `true` when its formatting is its own, not its column's or row's (Automatic in a currency column) |104+| `text` | a cell | How its `input` reads when its `format` would say otherwise: `true` for text typed into a Plain text cell whose format changed since (`{"input":"007","text":true}`), `false` for a number or formula typed before the cell was formatted as Plain text; either stays as typed, as in Sheets. Older builds read the input as the format says |104105 | `wrap`, `borders` | a cell or a line | How its text [wraps](../sheets/formatting.md#wrapping) and its [borders](../sheets/formatting.md#borders); older builds show the text overflowing, without lines |105106 | `valign`, and colors in `borders` | a cell or a line | Where its text sits in a [tall row](../sheets/formatting.md#vertical-alignment), and its borders' colors; older builds show the text at the bottom and the lines in the text's ink |106107 | `lines` | a sheet | [Column and row formats](#column-and-row-formats) |@@ -9,7 +9,13 @@ import (
99 // fileCell is a cell with formatting or a note as the file writes it; a1010 // cell with neither is just its input (see fileFormat).1111 type fileCell struct {12- Input string `json:"input,omitempty"`12+ Input string `json:"input,omitempty"`13+ // Text says how the input reads when that isn't what the format14+ // says: true for text typed into a Plain text cell whose format15+ // changed since, false for a number or formula typed before the cell16+ // was formatted as Plain text. It needs no version bump: earlier17+ // builds read the input as the format says.18+ Text *bool `json:"text,omitempty"`1319 Format string `json:"format,omitempty"`1420 Decimals *int `json:"decimals,omitempty"`1521 Pattern string `json:"pattern,omitempty"`@@ -32,11 +38,13 @@ type fileCell struct {
3238 }3339 3440 func encodeCell(c *Cell) (json.RawMessage, error) {35- if c.Format.IsZero() && c.Style.IsZero() && c.Note == "" {41+ text := c.readsAgainstFormat()42+ if c.Format.IsZero() && c.Style.IsZero() && c.Note == "" && text == nil {3643 return json.Marshal(c.Input)3744 }3845 fc := fileCell{3946 Input: c.Input,47+ Text: text,4048 Pattern: c.Format.Pattern,4149 Bold: c.Style.Bold,4250 Italic: c.Style.Italic,@@ -59,25 +67,57 @@ func encodeCell(c *Cell) (json.RawMessage, error) {
5967 return json.Marshal(fc)6068 }6169 70+// readsAgainstFormat is the file's text field for c: whether its entry71+// is text, when its format says otherwise (see fileCell), else nil.72+func (c *Cell) readsAgainstFormat() *bool {73+ text := c.Format.Kind == FmtText74+ switch {75+ case text && c.expr != nil, !text && c.typedText:76+ v := c.typedText77+ return &v78+ }79+ return nil80+}81+6282 // decodeCell reads a cell's entry and formatting, as line formats are6383 // stored too.6484 func decodeCell(raw json.RawMessage) (string, Format, Style, error) {65- input, f, st, _, err := decodeNoted(raw)66- return input, f, st, err85+ e, err := decodeNoted(raw)86+ return e.input, e.f, e.st, err87+}88+89+// fileEntry is a cell as the file has it.90+type fileEntry struct {91+ input string92+ f Format93+ st Style94+ note string95+ text *bool // see fileCell.Text96+}97+98+// cell builds the cell, as newCell does, reading its entry as text or99+// not as the file says.100+func (e fileEntry) cell(implied bool) (*Cell, error) {101+ asText := e.f.Kind == FmtText102+ if e.text != nil {103+ asText = *e.text104+ }105+ c, err := readEntry(e.input, e.f, e.st, implied, asText)106+ return c.withNote(CleanNote(e.note)), err67107 }68108 69-// decodeNoted is decodeCell with the cell's note.70-func decodeNoted(raw json.RawMessage) (string, Format, Style, string, error) {109+// decodeNoted is decodeCell with the cell's note and how it reads.110+func decodeNoted(raw json.RawMessage) (fileEntry, error) {71111 var input string72112 if err := json.Unmarshal(raw, &input); err == nil {73- return input, Format{}, Style{}, "", nil113+ return fileEntry{input: input}, nil74114 }75115 var fc fileCell76116 if err := json.Unmarshal(raw, &fc); err != nil {77- return "", Format{}, Style{}, "", err117+ return fileEntry{}, err78118 }79119 input, f, st, err := decodeFormatted(fc)80- return input, f, st, fc.Note, err120+ return fileEntry{input, f, st, fc.Note, fc.Text}, err81121 }82122 83123 // decodeFormatted reads the entry and formatting of a cell written as an@@ -62,3 +62,30 @@ func TestBatchJoinsFormattingSteps(t *testing.T) {
6262 t.Errorf("C1 format %+v", f)6363 }6464 }65+66+// An entry reads as it was typed until it's typed again: a number or67+// formula formatted as Plain text afterwards keeps computing, and text68+// typed into a Plain text cell stays text in another format or none,69+// in the file too, and a formula that doesn't parse stays text without70+// making the file unreadable.71+func TestPlainTextFormatKeepsEntries(t *testing.T) {72+ s := New()73+ s.Set(at("A1"), "=1+1")74+ s.Set(at("A2"), "5")75+ s.SetFormat(rect("A1:A2"), Format{Kind: FmtText})76+ s.SetFormat(rect("B1:B3"), Format{Kind: FmtText})77+ s.Set(at("B1"), "=1+1")78+ s.Set(at("B2"), "5")79+ s.Set(at("B3"), "=SUM(")80+ s.SetFormat(rect("B1:B1"), Format{Kind: FmtNumber, Decimals: 1})81+ s.ClearFormatting(rect("B2:B3"))82+ want := map[string]Value{"A1": num(2), "A2": num(5),83+ "B1": {Kind: Text, Str: "=1+1"}, "B2": {Kind: Text, Str: "5"}, "B3": {Kind: Text, Str: "=SUM("}}84+ for name, sh := range map[string]*Sheet{"edited": s, "reopened": roundTrip(t, s)} {85+ for a, v := range want {86+ if got := sh.Value(at(a)); got != v {87+ t.Errorf("%s: %s = %+v, want %+v", name, a, got, v)88+ }89+ }90+ }91+}@@ -330,9 +330,7 @@ func (s *Sheet) readCell(sc *scanner, a Addr, implied bool) error {
330330 if err != nil {331331 return err332332 }333- var input, note string334- var fm Format335- var st Style333+ var e fileEntry336334 if c == '"' {337335 body, plain, err := sc.text()338336 if err != nil {@@ -341,7 +339,7 @@ func (s *Sheet) readCell(sc *scanner, a Addr, implied bool) error {
341339 if plain && s.loadNum(a, body, Format{}, Style{}) {342340 return nil343341 }344- if input, err = unquote(body, plain); err != nil {342+ if e.input, err = unquote(body, plain); err != nil {345343 return err346344 }347345 } else {@@ -349,19 +347,19 @@ func (s *Sheet) readCell(sc *scanner, a Addr, implied bool) error {
349347 if err != nil {350348 return err351349 }352- if input, fm, st, note, err = decodeNoted(raw); err != nil {350+ if e, err = decodeNoted(raw); err != nil {353351 return err354352 }355- if note == "" && s.loadNum(a, []byte(input), fm, st) {353+ if e.note == "" && e.text == nil && s.loadNum(a, []byte(e.input), e.f, e.st) {356354 return nil357355 }358356 }359- cell, err := newCell(input, fm, st, implied)357+ cell, err := e.cell(implied)360358 if err != nil {361359 return err362360 }363361 // An empty entry leaves no cell, even where the key came before.364- if cell = cell.withNote(CleanNote(note)); cell != nil || s.cells.has(a) {362+ if cell != nil || s.cells.has(a) {365363 s.place(a, cell)366364 }367365 return nil@@ -80,15 +80,15 @@ func placeWhole(s *Sheet, cells map[string]json.RawMessage, version int) (ambigu
8080 }8181 ambiguous = ambiguous || seen[a]8282 seen[a] = true83- input, fm, st, note, err := decodeNoted(raw)83+ e, err := decodeNoted(raw)8484 if err != nil {8585 return false, err8686 }87- c, err := newCell(input, fm, st, version < 2)87+ c, err := e.cell(version < 2)8888 if err != nil {8989 return false, err9090 }91- if c = c.withNote(CleanNote(note)); c != nil {91+ if c != nil {9292 s.place(a, c)9393 }9494 }@@ -47,6 +47,11 @@ type Cell struct {
4747 volatile bool // expr calls TODAY, NOW, RAND...4848 derived bool // a pivot table's result, owned by the engine: see pivot.go4949 spilled bool // part of an array a formula spills, owned by it: see spill.go50+ // typedText marks text typed into a Plain text cell that would read51+ // as a number or formula, or not at all, typed into a cell of another52+ // format: it stays text when the cell's format changes, as in Sheets,53+ // until it's typed again.54+ typedText bool5055 }5156 5257 // Spilled reports whether the cell shows part of an array another@@ -274,6 +279,13 @@ func (s *Sheet) put(a Addr, input string) error {
274279 // is text. Control characters are dropped so a worksheet file can't275280 // smuggle escape sequences to the terminal.276281 func newCell(input string, f Format, st Style, implied bool) (*Cell, error) {282+ return readEntry(input, f, st, implied, f.Kind == FmtText)283+}284+285+// readEntry is newCell reading the entry as text when asText, and as286+// typed into a cell of another format than Plain text otherwise, as a287+// file says to of an entry typed before its cell's format changed.288+func readEntry(input string, f Format, st Style, implied, asText bool) (*Cell, error) {277289 input = strings.Map(func(r rune) rune {278290 if unicode.IsControl(r) {279291 return -1@@ -284,21 +296,28 @@ func newCell(input string, f Format, st Style, implied bool) (*Cell, error) {
284296 return formattingOnly(f, st), nil285297 }286298 var n Node287- if f.Kind != FmtText {299+ if !asText {288300 var fi Format289301 var err error290302 if n, fi, err = classify(input); err != nil {291303 return nil, err292304 }293- if implied && !fi.IsZero() {305+ if implied && !fi.IsZero() && f.Kind != FmtText {294306 f = fi295307 }296308 }297- c := &Cell{Input: input, Format: f, Style: st}309+ c := &Cell{Input: input, Format: f, Style: st, typedText: asText && readsOtherwise(input)}298310 c.setExpr(n)299311 return c, nil300312 }301313 314+// readsOtherwise reports whether an entry typed as text reads as315+// something else, or not at all, typed into a cell of another format.316+func readsOtherwise(input string) bool {317+ n, _, err := classify(input)318+ return n != nil || err != nil319+}320+302321 // formattingOnly is a blank cell holding formatting, or nil when there is303322 // none.304323 func formattingOnly(f Format, st Style) *Cell {@@ -150,7 +150,7 @@ var noLook look
150150 // strings it uses to the table.151151 func (st *cellStore) plainSlot(c *Cell) (slot, bool) {152152 switch {153- case c.Note != "":153+ case c.Note != "" || c.typedText:154154 return slot{}, false155155 case c.derived || c.spilled:156156 return st.derivedSlot(c)