1package fileio23import (4 "context"5 "os"6 "path/filepath"7 "slices"8 "strings"9 "testing"1011 "src.rest/cat/012/internal/locale"12 "src.rest/cat/012/internal/numfmt"13 "src.rest/cat/012/internal/sheet"14)1516func mustLocale(t *testing.T, tag string) *locale.Locale {17 t.Helper()18 l, ok := locale.Lookup(tag)19 if !ok {20 t.Fatalf("no locale %s", tag)21 }22 return l23}2425// TestReadDelimitedInLocales reads CSV files in a locale: its own26// separators and date order, or the other decimal separator when the27// file writes numbers that way.28func TestReadDelimitedInLocales(t *testing.T) {29 date := numfmt.DateSerial(2026, 9, 26)30 cases := []struct {31 name, tag, text string32 values map[string]float6433 texts map[string]string34 note string35 }{36 {"en-US reads decimal commas", "en-US", "name;price\napple;1,5\npear;2,25\n",37 map[string]float64{"B2": 1.5, "B3": 2.25}, nil, "numbers with decimal commas"},38 {"de-DE reads its own", "de-DE", "a;b;c\n1.234,5;26.09.2026;12,50 €\n",39 map[string]float64{"A2": 1234.5, "B2": date, "C2": 12.5}, nil, ""},40 {"de-DE ties go to semicolons", "de-DE", "1,5;2,5\n3,5;4,5\n",41 map[string]float64{"A1": 1.5, "B2": 4.5}, nil, ""},42 {"de-DE reads decimal points", "de-DE", "a,b\n1.25,2.75\n3.5,4.125\n",43 map[string]float64{"A2": 1.25, "B2": 2.75, "B3": 4.125}, nil, "numbers with decimal points"},44 {"en-GB reads its date order", "en-GB", "d\n26/09/2026\n",45 map[string]float64{"A2": date}, nil, ""},46 {"de-DE keeps its formulas text", "de-DE", "=SUM(1;2)\t-A1*0,5\n",47 nil, map[string]string{"A1": "=SUM(1;2)", "B1": "-A1*0,5"}, ""},48 }49 for _, c := range cases {50 t.Run(c.name, func(t *testing.T) {51 k := CSV52 if strings.Contains(c.text, "\t") {53 k = TSV54 }55 s, _, notes, err := readDelimited(context.Background(), strings.NewReader(c.text), k, 0, mustLocale(t, c.tag), func(int) {})56 if err != nil {57 t.Fatal(err)58 }59 want := map[string]sheet.Value{}60 for a, n := range c.values {61 want[a] = sheet.Value{Kind: sheet.Number, Num: n}62 }63 for a, s := range c.texts {64 want[a] = sheet.Value{Kind: sheet.Text, Str: s}65 }66 for a, w := range want {67 if v := s.Value(addr(t, a)); v != w {68 t.Errorf("%s = %v, want %v", a, v, w)69 }70 }71 if got := numbersNote(notes); got != c.note {72 t.Errorf("notes %q, want %q", notes, c.note)73 }74 })75 }76}7778// numbersNote is the note saying how numbers were read, or "".79func numbersNote(notes []string) string {80 for _, n := range notes {81 if strings.HasPrefix(n, "numbers") {82 return n83 }84 }85 return ""86}8788// TestExportDelimitedInLocale writes CSV as a locale shows it, with ;89// between fields where the decimal separator is a comma.90func TestExportDelimitedInLocale(t *testing.T) {91 src := build(t, map[string]string{"A1": "1234.5", "B1": "9/26/2026", "C1": "Tee, schwarz", "A2": "=A1/4"})92 src.SetFormat(sheet.NewRect(addr(t, "A1"), addr(t, "A1")), sheet.Preset(sheet.FmtCurrency))93 src.Book().SetLocale("de-DE")94 name := filepath.Join(t.TempDir(), "x.csv")95 res, err := Export(context.Background(), name, CSV, Snap(src, sheet.Rect{}, "x"), ExportOptions{})96 if err != nil {97 t.Fatal(err)98 }99 data, _ := os.ReadFile(name)100 if want := "1.234,50 €;26.9.2026;Tee, schwarz\n308,63\u00a0€;;\n"; string(data) != want {101 t.Errorf("wrote %q, want %q", data, want)102 }103 if !slices.Contains(res.Notes, "separated by semicolons") {104 t.Errorf("notes %q", res.Notes)105 }106 // Read back in de-DE, the values are the same.107 back, _, _, err := readDelimited(context.Background(), strings.NewReader(string(data)), CSV, 0, mustLocale(t, "de-DE"), func(int) {})108 if err != nil {109 t.Fatal(err)110 }111 for _, a := range []string{"A1", "B1"} {112 if got, want := back.Value(addr(t, a)), src.Value(addr(t, a)); got != want {113 t.Errorf("%s read back as %v, want %v", a, got, want)114 }115 }116}