1package sheet23import (4 "bytes"5 "math"6 "reflect"7 "strings"8 "testing"9)1011// budgetCells is a small table: labels down A, two series with headers.12var budgetCells = map[string]string{13 "A1": "Month", "B1": "Rent", "C1": "Food",14 "A2": "Jan", "B2": "1450", "C2": "600",15 "A3": "Feb", "B3": "1450", "C3": "",16 "A4": "Mar", "B4": "1500", "C4": "=C2+12",17}1819func rng(s string) Rect {20 r, ok := ParseRange(s)21 if !ok {22 panic(s)23 }24 return r25}2627// sameFloats compares with NaN equal to NaN.28func sameFloats(a, b []float64) bool {29 if len(a) != len(b) {30 return false31 }32 for i := range a {33 if a[i] != b[i] && !(math.IsNaN(a[i]) && math.IsNaN(b[i])) {34 return false35 }36 }37 return true38}3940func TestChartData(t *testing.T) {41 nan := math.NaN()42 s := sheetOf(t, budgetCells)43 tests := []struct {44 name string45 chart Chart46 cats []string47 names []string48 values [][]float6449 }{50 {"columns with header and labels", Chart{Data: rng("A1:C4"), Header: true, Labels: true},51 []string{"Jan", "Feb", "Mar"}, []string{"Rent", "Food"},52 [][]float64{{1450, 1450, 1500}, {600, nan, 612}}},53 {"no header: the first row is data", Chart{Data: rng("B2:C4")},54 []string{"1", "2", "3"}, []string{"Series 1", "Series 2"},55 [][]float64{{1450, 1450, 1500}, {600, nan, 612}}},56 {"series in rows", Chart{Data: rng("A1:C4"), ByRow: true, Header: true, Labels: true},57 []string{"Rent", "Food"}, []string{"Jan", "Feb", "Mar"},58 [][]float64{{1450, 600}, {1450, nan}, {1500, 612}}},59 {"whole columns stop at the data", Chart{Data: rng("B1:B8192"), Header: true},60 []string{"1", "2", "3"}, []string{"Rent"},61 [][]float64{{1450, 1450, 1500}}},62 {"text in a series is a gap", Chart{Data: rng("A2:B4")},63 []string{"1", "2", "3"}, []string{"Series 1", "Series 2"},64 [][]float64{{nan, nan, nan}, {1450, 1450, 1500}}},65 }66 for _, tt := range tests {67 t.Run(tt.name, func(t *testing.T) {68 d := s.ChartData(tt.chart)69 if !reflect.DeepEqual(d.Categories, tt.cats) {70 t.Errorf("categories %q, want %q", d.Categories, tt.cats)71 }72 if len(d.Series) != len(tt.names) {73 t.Fatalf("%d series, want %d", len(d.Series), len(tt.names))74 }75 for i, sr := range d.Series {76 if sr.Name != tt.names[i] || !sameFloats(sr.Values, tt.values[i]) {77 t.Errorf("series %d = %q %v, want %q %v", i, sr.Name, sr.Values, tt.names[i], tt.values[i])78 }79 }80 })81 }82}8384func TestChartDataFormat(t *testing.T) {85 s := sheetOf(t, map[string]string{"A1": "$1,200", "A2": "$900"})86 if d := s.ChartData(Chart{Data: rng("A1:A2")}); d.Format.Kind != FmtCurrency {87 t.Errorf("format %v, want currency", d.Format.Kind)88 }89}9091func TestGuessChart(t *testing.T) {92 s := sheetOf(t, budgetCells)93 tests := []struct {94 r string95 header, labels bool96 title string97 }{98 {"A1:C4", true, true, "Rent and Food"},99 {"B1:B4", true, false, "Rent"},100 {"B2:C4", false, false, ""},101 {"A2:B4", false, true, ""},102 {"A1:D9", true, true, "Rent and Food"}, // trimmed to the data103 }104 for _, tt := range tests {105 c := s.GuessChart(rng(tt.r))106 if c.Header != tt.header || c.Labels != tt.labels || c.Title != tt.title || c.Type != ChartColumn {107 t.Errorf("%s: header %v labels %v title %q, want %v %v %q", tt.r, c.Header, c.Labels, c.Title, tt.header, tt.labels, tt.title)108 }109 }110}111112func TestRegion(t *testing.T) {113 s := sheetOf(t, map[string]string{114 "B2": "x", "C2": "1", "C3": "2", "D4": "3", // D4 touches C3 diagonally115 "F2": "far",116 })117 tests := []struct{ from, want string }{118 {"C3", "B2:D4"},119 {"B2", "B2:D4"},120 {"F2", "F2"},121 {"H9", "H9"},122 }123 for _, tt := range tests {124 if got := s.Region(at(tt.from)).String(); got != tt.want {125 t.Errorf("Region(%s) = %s, want %s", tt.from, got, tt.want)126 }127 }128}129130func TestChartUndo(t *testing.T) {131 s := sheetOf(t, budgetCells)132 c := s.GuessChart(rng("A1:C4"))133 c.At, c.W, c.H = at("E2"), 40, 12134 i := s.AddChart(c)135 moved := c136 moved.At = at("F9")137 s.SetChart(i, moved, "move chart")138 s.DeleteChart(i)139 if len(s.Charts()) != 0 {140 t.Fatal("chart not deleted")141 }142 for _, want := range []struct {143 label string144 at string145 n int146 }{{"delete chart", "F9", 1}, {"move chart", "E2", 1}, {"insert chart", "", 0}} {147 ch, ok := s.Undo()148 if !ok || ch.Label != want.label {149 t.Fatalf("undo %q, want %q", ch.Label, want.label)150 }151 if got := s.Charts(); len(got) != want.n || want.n > 0 && got[0].At.String() != want.at {152 t.Fatalf("after undoing %s: %+v", want.label, got)153 }154 }155 s.Redo()156 if got := s.Charts(); len(got) != 1 || got[0].At != at("E2") {157 t.Fatalf("redo: %+v", got)158 }159 // Setting a chart to what it is isn't a step.160 id := s.StateID()161 s.SetChart(0, s.Charts()[0], "edit chart")162 if s.StateID() != id {163 t.Error("no-op edit made an undo step")164 }165}166167func TestChartsFollowRowsAndColumns(t *testing.T) {168 s := sheetOf(t, budgetCells)169 s.AddChart(Chart{Data: rng("A1:C4"), At: at("E2"), W: 30, H: 10})170 s.InsertRows(2, 1)171 s.InsertCols(0, 2)172 c := s.Charts()[0]173 if c.Data.String() != "C1:E5" || c.At.String() != "G2" {174 t.Errorf("after inserts: data %s at %s", c.Data, c.At)175 }176 s.DeleteCols(2, 3)177 if n := len(s.Charts()); n != 0 {178 t.Errorf("chart over deleted columns kept (%d)", n)179 }180 s.Undo()181 if n := len(s.Charts()); n != 1 {182 t.Errorf("undo didn't bring the chart back (%d)", n)183 }184}185186func TestChartFileRoundTrip(t *testing.T) {187 s := sheetOf(t, budgetCells)188 want := []Chart{189 {Type: ChartLine, Data: rng("A1:C4"), Header: true, Labels: true, Title: "Spend", At: at("E2"), W: 44, H: 14},190 {Type: ChartPie, Data: rng("B2:B4"), ByRow: true, At: at("A20"), W: 30, H: 12},191 {Type: ChartArea, Data: rng("A1:C4"), At: at("A30"), W: 30, H: 12,192 ChartOptions: ChartOptions{Stack: StackPercent, Min: -5, HasMin: true, Max: 0, HasMax: true, NoGrid: true, Legend: LegendRight}},193 {Type: ChartScatter, Data: rng("B1:C4"), At: at("A45"), W: 30, H: 12,194 ChartOptions: ChartOptions{Trend: true, Log: true, Legend: LegendNone}},195 }196 for _, c := range want {197 s.AddChart(c)198 }199 var buf bytes.Buffer200 if err := s.Write(&buf); err != nil {201 t.Fatal(err)202 }203 if !strings.Contains(buf.String(), `"charts": [`+"\n "+`{"type":"line","data":"A1:C4","at":"E2","width":44,"height":14,"header":true,"labels":true,"title":"Spend"}`) {204 t.Errorf("charts not one per line:\n%s", buf.String())205 }206 // Options at their defaults are left out; the others are fields.207 if !strings.Contains(buf.String(), `"stack":"percent","min":-5,"max":0,"gridlines":false,"legend":"right"}`) ||208 !strings.Contains(buf.String(), `"type":"scatter","data":"B1:C4","at":"A45","width":30,"height":12,"trend":true,"log":true,"legend":"none"}`) {209 t.Errorf("chart options:\n%s", buf.String())210 }211 got, err := Read(&buf)212 if err != nil {213 t.Fatal(err)214 }215 if !reflect.DeepEqual(got.Charts(), want) {216 t.Errorf("round trip:\n got %+v\nwant %+v", got.Charts(), want)217 }218 // Older files have no charts field.219 old, err := Read(strings.NewReader(`{"version": 2, "cells": {"A1": "1"}}`))220 if err != nil || len(old.Charts()) != 0 {221 t.Errorf("old file: %v %v", err, old.Charts())222 }223 if _, err := Read(strings.NewReader(`{"version": 2, "cells": {}, "charts": [{"type": "radar", "data": "A1", "at": "A1"}]}`)); err == nil {224 t.Error("unknown chart type accepted")225 }226}