1package value_test23import (4 "fmt"5 "os"6 "strings"7 "testing"89 "src.rest/cat/012/internal/locale"10 "src.rest/cat/012/internal/numfmt"11 "src.rest/cat/012/internal/value"12)1314func loc(t *testing.T, tag string) *locale.Locale {15 t.Helper()16 l, ok := locale.Lookup(tag)17 if !ok {18 t.Fatalf("no locale %s", tag)19 }20 return l21}2223func TestParseValueInLocales(t *testing.T) {24 date := numfmt.DateSerial(2026, 9, 26)25 cases := []struct {26 tag, typed string27 ok bool28 v float6429 kind value.FormatKind30 canonical string31 }{32 {"en-US", "1,234.5", true, 1234.5, value.FmtNumber, "1,234.5"},33 {"en-US", "1,5", false, 0, 0, ""},34 {"de-DE", "1.234,5", true, 1234.5, value.FmtNumber, "1,234.5"},35 {"de-DE", "1,5", true, 1.5, value.FmtAuto, "1.5"},36 {"de-DE", "-0,25", true, -0.25, value.FmtAuto, "-0.25"},37 {"de-DE", "12,5 %", true, 0.125, value.FmtPercent, "12.5%"},38 {"de-DE", "1.234,50 €", true, 1234.5, value.FmtCurrency, "$1,234.50"},39 {"de-DE", "-5 €", true, -5, value.FmtCurrency, "-$5"},40 {"de-DE", "1,5E3", true, 1500, value.FmtScientific, "1.5E3"},41 {"de-DE", "1,234.5", false, 0, 0, ""},42 {"de-DE", "$5", false, 0, 0, ""},43 {"de-DE", "26.09.2026", true, date, value.FmtDate, "09/26/2026"},44 {"de-DE", "26.9.2026.", true, date, value.FmtDate, "9/26/2026"},45 {"de-DE", "26/09/2026", true, date, value.FmtDate, "09/26/2026"},46 {"de-DE", "9/26/2026", false, 0, 0, ""},47 {"de-DE", "2026-09-26", true, date, value.FmtDate, "2026-09-26"},48 {"de-DE", "Sep 26, 2026", true, date, value.FmtDate, "Sep 26, 2026"},49 {"de-DE", "26.09.2026 14:30", true, date + numfmt.TimeSerial(14, 30, 0), value.FmtDateTime, "09/26/2026 14:30"},50 {"de-DE", "14:30", true, numfmt.TimeSerial(14, 30, 0), value.FmtTime, "14:30"},51 {"en-GB", "26/09/2026", true, date, value.FmtDate, "09/26/2026"},52 {"en-GB", "9/26/2026", false, 0, 0, ""},53 {"en-GB", "£1,200", true, 1200, value.FmtCurrency, "$1,200"},54 {"en-GB", "$1,200", false, 0, 0, ""},55 {"en-GB", "1.5", true, 1.5, value.FmtAuto, "1.5"},56 {"fr-FR", "1 234,5", true, 1234.5, value.FmtNumber, "1,234.5"},57 {"fr-FR", "1 234,5", true, 1234.5, value.FmtNumber, "1,234.5"},58 {"fr-FR", "12 %", true, 0.12, value.FmtPercent, "12%"},59 {"fr-FR", "3,50 €", true, 3.5, value.FmtCurrency, "$3.50"},60 {"fr-FR", "1.5", false, 0, 0, ""},61 {"es-ES", "1.000,25", true, 1000.25, value.FmtNumber, "1,000.25"},62 {"it-IT", "26/9", true, numfmt.DateSerial(value.Now().Year(), 9, 26), value.FmtDate, "9/26"},63 {"pt-BR", "R$ 1.234,56", true, 1234.56, value.FmtCurrency, "$1,234.56"},64 {"nl-NL", "26-9-2026", true, date, value.FmtDate, "9/26/2026"},65 {"nl-NL", "€ 5", true, 5, value.FmtCurrency, "$5"},66 {"sv-SE", "2026-09-26", true, date, value.FmtDate, "09/26/2026"},67 {"sv-SE", "100 kr", true, 100, value.FmtCurrency, "$100"},68 {"pl-PL", "1 000,5 zł", true, 1000.5, value.FmtCurrency, "$1,000.5"},69 {"de-CH", "1'234.5", true, 1234.5, value.FmtNumber, "1,234.5"},70 {"de-CH", "CHF 5", true, 5, value.FmtCurrency, "$5"},71 {"ja-JP", "2026/9/26", true, date, value.FmtDate, "9/26/2026"},72 {"ja-JP", "¥1,000", true, 1000, value.FmtCurrency, "$1,000"},73 {"zh-CN", "26/9/26", true, date, value.FmtDate, "9/26/26"},74 {"de-CH", "26.9", true, 26.9, value.FmtAuto, "26.9"},75 {"de-DE", "26. September 2026", true, date, value.FmtDate, "26 September 2026"},76 {"de-DE", "26 Sep 2026", true, date, value.FmtDate, "26 Sep 2026"},77 {"de-DE", "26. Okt. 2026", true, date + 30, value.FmtDate, "26 Oct 2026"},78 {"fr-FR", "26 sept. 2026", true, date, value.FmtDate, "26 Sep 2026"},79 {"pl-PL", "26 września 2026", true, date, value.FmtDate, "26 September 2026"},80 {"cs-CZ", "26. září 2026", true, date, value.FmtDate, "26 September 2026"},81 {"es-ES", "26 de septiembre", false, 0, 0, ""},82 }83 for _, c := range cases {84 l := loc(t, c.tag)85 v, f, ok := value.ParseValueIn(c.typed, l)86 if ok != c.ok {87 t.Errorf("%s %q: parsed %v, want %v", c.tag, c.typed, ok, c.ok)88 continue89 }90 if !ok {91 continue92 }93 if v != c.v || f.Kind != c.kind {94 t.Errorf("%s %q = %v %v, want %v %v", c.tag, c.typed, v, f.Kind, c.v, c.kind)95 }96 got, _ := value.Canonicalize(c.typed, l)97 if got != c.canonical {98 t.Errorf("%s %q: canonical %q, want %q", c.tag, c.typed, got, c.canonical)99 }100 }101}102103// TestLocalizeRoundTrips checks that every entry as stored, written in104// each locale's form, is typed back to the same value and format.105func TestLocalizeRoundTrips(t *testing.T) {106 stored := []string{"1234.5", "1,234.5", "$1,234.50", "-$5", "-12.5%", "1.5E3", "0.001",107 "9/26/2026", "09/26/2026", "9/26", "2026-09-26", "Sep 26, 2026", "26 Sep 2026",108 "14:30", "2:30 PM", "25:30", "9/26/2026 14:30:05", "14:30:15.5"}109 for _, l := range locale.All() {110 for _, s := range stored {111 want, wf, ok := value.ParseValue(s)112 if !ok {113 t.Fatalf("%q isn't a value", s)114 }115 shown := value.Localize(s, l)116 got, gf, ok := value.ParseValueIn(shown, l)117 if !ok || got != want || gf.Kind != wf.Kind {118 t.Errorf("%s: %q shown as %q reads %v %v %v, want %v %v", l.Tag, s, shown, got, gf.Kind, ok, want, wf.Kind)119 }120 }121 }122}123124func TestLocalizeForms(t *testing.T) {125 cases := []struct{ tag, stored, shown string }{126 {"en-US", "$1,234.50", "$1,234.50"},127 {"de-DE", "$1,234.50", "1.234,50 €"},128 {"de-DE", "-12.5%", "-12,5%"},129 {"de-DE", "9/26/2026", "26.9.2026"},130 {"de-DE", "9/26/2026 14:30:15.5", "26.9.2026 14:30:15,5"},131 {"de-DE", "2026-09-26", "2026-09-26"},132 {"de-DE", "hello", "hello"},133 {"en-GB", "$5", "£5"},134 {"fr-FR", "1,234.5", "1 234,5"},135 {"pt-BR", "$5", "R$ 5"},136 {"ja-JP", "9/26/2026", "2026/9/26"},137 {"nl-NL", "9/26", "26-9"},138 }139 for _, c := range cases {140 if got := value.Localize(c.stored, loc(t, c.tag)); got != c.shown {141 t.Errorf("%s %q shown as %q, want %q", c.tag, c.stored, got, c.shown)142 }143 }144}145146func TestDisplayInLocales(t *testing.T) {147 v := 1234.5148 date := numfmt.DateSerial(2026, 9, 26) + numfmt.TimeSerial(14, 5, 0)149 cases := []struct {150 tag string151 f value.Format152 n float64153 want string154 }{155 {"en-US", value.Preset(value.FmtNumber), v, "1,234.50"},156 {"en-US", value.Preset(value.FmtCurrency), v, "$1,234.50"},157 {"en-US", value.Preset(value.FmtDate), date, "9/26/2026"},158 {"en-US", value.Preset(value.FmtTime), date, "2:05:00 PM"},159 {"de-DE", value.Preset(value.FmtNumber), v, "1.234,50"},160 {"de-DE", value.Preset(value.FmtPercent), 0.125, "12,50%"},161 {"de-DE", value.Preset(value.FmtScientific), v, "1,23E+03"},162 {"de-DE", value.Preset(value.FmtCurrency), -v, "-1.234,50 €"},163 {"de-DE", value.Preset(value.FmtDate), date, "26.9.2026"},164 {"de-DE", value.Preset(value.FmtTime), date, "14:05:00"},165 {"de-DE", value.Preset(value.FmtDateTime), date, "26.9.2026 14:05:00"},166 {"de-DE", value.Format{Kind: value.FmtDate, Pattern: "yyyy-mm-dd"}, date, "2026-09-26"},167 {"de-DE", value.Format{Kind: value.FmtCustom, Pattern: `"$"#,##0.0`}, v, "$1.234,5"},168 {"fr-FR", value.Preset(value.FmtNumber), v, "1 234,50"},169 {"en-GB", value.Preset(value.FmtCurrency), v, "£1,234.50"},170 {"en-GB", value.Preset(value.FmtDate), date, "26/9/2026"},171 {"pt-BR", value.Preset(value.FmtCurrency), v, "R$ 1.234,50"},172 {"sv-SE", value.Preset(value.FmtDate), date, "2026-09-26"},173 {"ja-JP", value.Preset(value.FmtDate), date, "2026/9/26"},174 {"de-CH", value.Preset(value.FmtNumber), v, "1’234.50"},175 }176 for _, c := range cases {177 l := loc(t, c.tag)178 if got := numfmt.FormatIn(c.n, c.f.CodeIn(l), l); got != c.want {179 t.Errorf("%s %v: %q, want %q", c.tag, c.f, got, c.want)180 }181 }182}183184func TestAccountingInLocales(t *testing.T) {185 cases := []struct {186 tag string187 v float64188 want string189 }{190 {"en-US", 1234.5, " $ 1,234.50 "},191 {"en-US", -1234.5, " $ (1,234.50)"},192 {"de-DE", 1234.5, " 1.234,50 € "},193 {"de-DE", 0, " - € "},194 {"pt-BR", 5, " R$ 5,00 "},195 }196 for _, c := range cases {197 got, ok := numfmt.AccountingIn(c.v, 2, 13, loc(t, c.tag))198 if !ok || got != c.want {199 t.Errorf("%s %v: %q %v, want %q", c.tag, c.v, got, ok, c.want)200 }201 }202}203204// TestLocaleDocTable checks the table of locales in docs/sheets/locale.md205// shows each as it renders.206func TestLocaleDocTable(t *testing.T) {207 doc, err := os.ReadFile("../../docs/sheets/locale.md")208 if err != nil {209 t.Fatal(err)210 }211 var want strings.Builder212 date := numfmt.DateSerial(2026, 9, 26)213 for _, l := range locale.All() {214 fmt.Fprintf(&want, "| `%s` | %s | %s | %s | %s |\n", l.Tag, l.Name,215 numfmt.FormatIn(1234.56, "#,##0.00", l),216 numfmt.FormatIn(date, value.Preset(value.FmtDate).CodeIn(l), l),217 numfmt.FormatIn(1234.56, value.Preset(value.FmtCurrency).CodeIn(l), l))218 }219 if !strings.Contains(string(doc), want.String()) {220 t.Errorf("docs/sheets/locale.md's table isn't:\n%s", want.String())221 }222}