cat / 012
clone

a spreadsheet for the terminal: lotus 1-2-3 looks, google sheets keys and formulas, one go binary

main
012 / internal / functions / doc_test.go
doc_test.go 4.0K
1package functions23import (4 "flag"5 "fmt"6 "go/ast"7 goparser "go/parser"8 gotoken "go/token"9 "os"10 "path/filepath"11 "slices"12 "strconv"13 "strings"14 "testing"15)1617var updateDocs = flag.Bool("update-docs", false, "rewrite docs/reference/functions.md from the function table")1819// docsPath is the generated function reference.20const docsPath = "../../docs/reference/functions.md"2122// categories names the group of the functions each file defines, in the23// order the reference lists them.24var categories = []struct{ file, title string }{25 {"everyday.go", "Everyday"},26 {"math.go", "Math"},27 {"stats.go", "Statistics"},28 {"logic.go", "Logic and information"},29 {"text.go", "Text"},30 {"regex.go", "Split and regular expressions"},31 {"lookup.go", "Lookup"},32 {"dynamic.go", "Arrays"},33 {"lambda.go", "LET and LAMBDA"},34 {"date.go", "Date and time"},35 {"finance.go", "Finance"},36 {"link.go", "Links"},37 {"jev.go", "JEV (hosted model)"},38}3940// TestFunctionsDoc keeps docs/reference/functions.md in step with the function41// table, as help is: run `go test ./internal/functions -run FunctionsDoc42// -update-docs` after adding a function.43func TestFunctionsDoc(t *testing.T) {44 byFile, err := definingFiles()45 if err != nil {46 t.Fatal(err)47 }48 got := functionsDoc(byFile)49 if *updateDocs {50 if err := os.WriteFile(docsPath, []byte(got), 0o644); err != nil {51 t.Fatal(err)52 }53 return54 }55 want, err := os.ReadFile(docsPath)56 if err != nil || string(want) != got {57 t.Errorf("docs/reference/functions.md is stale; run go test ./internal/functions -run FunctionsDoc -update-docs")58 }59}6061// definingFiles maps each function name to the file whose FuncDef62// literal defines it.63func definingFiles() (map[string]string, error) {64 fset := gotoken.NewFileSet()65 out := map[string]string{}66 files, err := filepath.Glob("*.go")67 if err != nil {68 return nil, err69 }70 for _, path := range files {71 if strings.HasSuffix(path, "_test.go") {72 continue73 }74 f, err := goparser.ParseFile(fset, path, nil, 0)75 if err != nil {76 return nil, err77 }78 ast.Inspect(f, func(n ast.Node) bool {79 kv, ok := n.(*ast.KeyValueExpr)80 if !ok {81 return true82 }83 if id, ok := kv.Key.(*ast.Ident); ok && id.Name == "Name" {84 if lit, ok := kv.Value.(*ast.BasicLit); ok && lit.Kind == gotoken.STRING {85 if name, err := strconv.Unquote(lit.Value); err == nil {86 out[name] = path87 }88 }89 }90 return true91 })92 }93 return out, nil94}9596func functionsDoc(byFile map[string]string) string {97 var b strings.Builder98 b.WriteString("---\ntitle: \"Functions\"\nsidebar_position: 3\n---\n\n# Functions\n\n")99 b.WriteString("<!-- Generated from the engine's function table by TestFunctionsDoc; do not edit. -->\n\n")100 fmt.Fprintf(&b, "012 has %d functions. They follow Google Sheets' names, arguments and\n", len(Funcs()))101 b.WriteString("semantics; `[brackets]` mark optional arguments. Function names are\n")102 b.WriteString("case-insensitive, and 1-2-3's `@SUM(A1..A5)` spelling still works.\n\n")103 var aliasList []string104 for a, canon := range aliases {105 aliasList = append(aliasList, fmt.Sprintf("`%s` for `%s`", a, canon))106 }107 slices.Sort(aliasList)108 if len(aliasList) > 0 {109 fmt.Fprintf(&b, "Aliases: %s.\n\n", strings.Join(aliasList, ", "))110 }111 listed := map[string]bool{}112 section := func(title string, fns []*FuncDef) {113 if len(fns) == 0 {114 return115 }116 fmt.Fprintf(&b, "## %s\n\n| Function | Description |\n|---|---|\n", title)117 for _, f := range fns {118 sig := f.Name + "(" + f.Args + ")"119 desc := f.Desc120 if f.Volatile {121 desc += " (recalculates on every change)"122 }123 fmt.Fprintf(&b, "| `%s` | %s |\n", strings.ReplaceAll(sig, "|", `\|`), desc)124 listed[f.Name] = true125 }126 b.WriteString("\n")127 }128 for _, c := range categories {129 var fns []*FuncDef130 for _, f := range Funcs() {131 if byFile[f.Name] == c.file {132 fns = append(fns, f)133 }134 }135 section(c.title, fns)136 }137 var rest []*FuncDef138 for _, f := range Funcs() {139 if !listed[f.Name] {140 rest = append(rest, f)141 }142 }143 section("Other", rest)144 return b.String()145}