- commit
- 17704157b87f766535290a151c5ea259aa92afe5
- parent
- 9e0a131f7a30c1997cd68c4ef698a81c8258ed96
- author
- Dami <dami.etoile@gmail.com>
| A | docs/ | 180 | ++++++++++++++++++++++++++++++ |
| A | internal/ | 142 | ++++++++++++++++++++++++ |
@@ -0,0 +1,180 @@
1+# Functions2+3+<!-- Generated from the engine's function table by TestFunctionsDoc; do not edit. -->4+5+012 has 120 functions. They follow Google Sheets' names, arguments and6+semantics; `[brackets]` mark optional arguments. Function names are7+case-insensitive, and 1-2-3's `@SUM(A1..A5)` spelling still works.8+9+Aliases: `AVG` for `AVERAGE`.10+11+## Everyday12+13+| Function | Description |14+|---|---|15+| `ABS(value)` | Absolute value |16+| `AND(logical1, [logical2, ...])` | TRUE if all arguments are true |17+| `AVERAGE(value1, [value2, ...])` | Average of numbers, ignoring text |18+| `COUNT(value1, [value2, ...])` | Count of numeric values |19+| `COUNTA(value1, [value2, ...])` | Count of non-empty values |20+| `FALSE()` | The logical value FALSE |21+| `IF(condition, value_if_true, [value_if_false])` | Choose a value by a condition |22+| `IFERROR(value, [value_if_error])` | A fallback when a value is an error |23+| `INT(value)` | Round down to the nearest integer |24+| `MAX(value1, [value2, ...])` | Largest number |25+| `MIN(value1, [value2, ...])` | Smallest number |26+| `MOD(dividend, divisor)` | Remainder, with the sign of the divisor |27+| `NA()` | The #N/A error |28+| `NOT(logical)` | The opposite of a logical value |29+| `OR(logical1, [logical2, ...])` | TRUE if any argument is true |30+| `PI()` | The number pi |31+| `ROUND(value, [places])` | Round to a number of decimal places, halves away from zero |32+| `SQRT(value)` | Square root |33+| `SUM(value1, [value2, ...])` | Sum of numbers |34+| `TRUE()` | The logical value TRUE |35+36+## Math37+38+| Function | Description |39+|---|---|40+| `CEILING(value, [factor])` | Round up to a multiple of factor |41+| `EXP(exponent)` | e raised to a power |42+| `FLOOR(value, [factor])` | Round down to a multiple of factor |43+| `LN(value)` | Natural logarithm |44+| `LOG(value, [base])` | Logarithm, base 10 by default |45+| `LOG10(value)` | Base-10 logarithm |46+| `POWER(base, exponent)` | A number raised to a power |47+| `PRODUCT(factor1, [factor2, ...])` | Product of numbers |48+| `QUOTIENT(dividend, divisor)` | Integer part of a division |49+| `RAND()` | A random number from 0 up to 1, new on every change (recalculates on every change) |50+| `RANDBETWEEN(low, high)` | A random integer between two values, new on every change (recalculates on every change) |51+| `ROUNDDOWN(value, [places])` | Round toward zero |52+| `ROUNDUP(value, [places])` | Round away from zero |53+| `SIGN(value)` | 1, 0 or -1 by the sign of a number |54+| `SUMIF(range, criterion, [sum_range])` | Sum of the cells that meet a condition |55+| `SUMIFS(sum_range, criteria_range1, criterion1, [criteria_range2, criterion2, ...])` | Sum of the cells that meet every condition |56+| `SUMPRODUCT(array1, [array2, ...])` | Sum of the products of matching entries |57+| `TRUNC(value, [places])` | Drop decimals past a number of places |58+59+## Statistics60+61+| Function | Description |62+|---|---|63+| `AVERAGEIF(criteria_range, criterion, [average_range])` | Average of the cells that meet a condition |64+| `AVERAGEIFS(average_range, criteria_range1, criterion1, [criteria_range2, criterion2, ...])` | Average of the cells that meet every condition |65+| `COUNTBLANK(range)` | Count of empty cells, including empty text |66+| `COUNTIF(range, criterion)` | Count of the cells that meet a condition |67+| `COUNTIFS(criteria_range1, criterion1, [criteria_range2, criterion2, ...])` | Count of the cells that meet every condition |68+| `LARGE(data, n)` | The nth largest number |69+| `MEDIAN(value1, [value2, ...])` | Middle value of numbers |70+| `MODE(value1, [value2, ...])` | Most common number (the first, on a tie) |71+| `RANK(value, data, [is_ascending])` | Rank of a number among others, largest first by default |72+| `SMALL(data, n)` | The nth smallest number |73+| `STDEV(value1, [value2, ...])` | Standard deviation of a sample |74+| `STDEVP(value1, [value2, ...])` | Standard deviation of a whole population |75+| `VAR(value1, [value2, ...])` | Variance of a sample |76+| `VARP(value1, [value2, ...])` | Variance of a whole population |77+78+## Logic and information79+80+| Function | Description |81+|---|---|82+| `IFNA(value, value_if_na)` | A fallback when a value is #N/A |83+| `IFS(condition1, value1, [condition2, value2, ...])` | The value of the first true condition |84+| `ISBLANK(value)` | TRUE if a cell is empty |85+| `ISERR(value)` | TRUE if a value is an error other than #N/A |86+| `ISERROR(value)` | TRUE if a value is any error |87+| `ISLOGICAL(value)` | TRUE if a value is TRUE or FALSE |88+| `ISNA(value)` | TRUE if a value is #N/A |89+| `ISNUMBER(value)` | TRUE if a value is a number |90+| `ISTEXT(value)` | TRUE if a value is text |91+| `SWITCH(expression, case1, value1, [case2, value2, ...], [default])` | The value for the first case equal to an expression |92+| `XOR(logical1, [logical2, ...])` | TRUE if an odd number of arguments are true |93+94+## Text95+96+| Function | Description |97+|---|---|98+| `CONCAT(value1, value2)` | Join two values as text |99+| `CONCATENATE(string1, [string2, ...])` | Join text, including every cell of ranges |100+| `EXACT(string1, string2)` | TRUE if two texts are identical, case included |101+| `FIND(search_for, text_to_search, [starting_at])` | Position of text, case-sensitive |102+| `LEFT(string, [number_of_characters])` | The first characters of text |103+| `LEN(text)` | Number of characters in text |104+| `LOWER(text)` | Text in lower case |105+| `MID(string, starting_at, extract_length)` | Characters from the middle of text |106+| `PROPER(text)` | Text with each word capitalized |107+| `REPLACE(text, position, length, new_text)` | Replace characters at a position |108+| `REPT(text, number_of_repetitions)` | Text repeated a number of times |109+| `RIGHT(string, [number_of_characters])` | The last characters of text |110+| `SEARCH(search_for, text_to_search, [starting_at])` | Position of text, ignoring case, with * and ? wildcards |111+| `SUBSTITUTE(text, search_for, replace_with, [occurrence_number])` | Replace occurrences of text |112+| `TEXT(number, format)` | A number as text in a format, e.g. "$#,##0.00" or "yyyy-mm-dd" |113+| `TEXTJOIN(delimiter, ignore_empty, text1, [text2, ...])` | Join text with a delimiter |114+| `TRIM(text)` | Text without leading, trailing and repeated spaces |115+| `UPPER(text)` | Text in upper case |116+| `VALUE(text)` | Text as a number; dates and times too |117+118+## Lookup119+120+| Function | Description |121+|---|---|122+| `CHOOSE(index, choice1, [choice2, ...])` | The choice at a position |123+| `COLUMNS(range)` | Number of columns in a range |124+| `HLOOKUP(search_key, range, index, [is_sorted])` | Find a key in the first row and return a value from its column |125+| `INDEX(reference, [row], [column])` | The value at a row and column of a range |126+| `MATCH(search_key, range, [search_type])` | Position of a key in a row or column |127+| `ROWS(range)` | Number of rows in a range |128+| `VLOOKUP(search_key, range, index, [is_sorted])` | Find a key in the first column and return a value from its row |129+| `XLOOKUP(search_key, lookup_range, result_range, [missing_value], [match_mode], [search_mode])` | Find a key and return the matching entry of another range |130+131+## Date and time132+133+| Function | Description |134+|---|---|135+| `DATE(year, month, day)` | A date from its parts; months and days past the end roll over |136+| `DATEDIF(start_date, end_date, unit)` | Time between dates in "Y", "M", "D", "MD", "YM" or "YD" |137+| `DATEVALUE(date_string)` | The date a text such as "2026-09-26" stands for |138+| `DAY(date)` | Day of the month of a date |139+| `DAYS(end_date, start_date)` | Number of days between two dates |140+| `EDATE(start_date, months)` | The same day a number of months away |141+| `EOMONTH(start_date, months)` | The last day of the month a number of months away |142+| `HOUR(time)` | Hour of a time, 0 to 23 |143+| `MINUTE(time)` | Minute of a time, 0 to 59 |144+| `MONTH(date)` | Month of a date, 1 to 12 |145+| `NETWORKDAYS(start_date, end_date, [holidays])` | Number of weekdays between two dates, counting both |146+| `NOW()` | The current date and time, updated on every change (recalculates on every change) |147+| `SECOND(time)` | Second of a time, 0 to 59 |148+| `TIME(hour, minute, second)` | A time of day from its parts |149+| `TIMEVALUE(time_string)` | The time of day a text such as "2:30 PM" stands for |150+| `TODAY()` | Today's date, updated on every change (recalculates on every change) |151+| `WEEKDAY(date, [type])` | Day of the week: 1 is Sunday, or Monday with type 2 |152+| `YEAR(date)` | Year of a date |153+154+## Finance155+156+| Function | Description |157+|---|---|158+| `FV(rate, number_of_periods, payment_amount, [present_value], [end_or_beginning])` | Future value of a series of payments |159+| `IRR(cashflow_amounts, [rate_guess])` | Internal rate of return of periodic cash flows |160+| `NPER(rate, payment_amount, present_value, [future_value], [end_or_beginning])` | Number of periods to pay off a loan or reach a value |161+| `NPV(discount, cashflow1, [cashflow2, ...])` | Net present value of periodic cash flows |162+| `PMT(rate, number_of_periods, present_value, [future_value], [end_or_beginning])` | Payment per period of a loan or investment |163+| `PV(rate, number_of_periods, payment_amount, [future_value], [end_or_beginning])` | Present value of a series of payments |164+| `RATE(number_of_periods, payment_per_period, present_value, [future_value], [end_or_beginning], [rate_guess])` | Interest rate per period of an annuity |165+166+## Links167+168+| Function | Description |169+|---|---|170+| `HYPERLINK(url, [link_label])` | A link that opens url, shown as its label |171+172+## JEV (hosted model)173+174+| Function | Description |175+|---|---|176+| `JEV.CLASSIFY(value, question, labels, [descriptions])` | The label that fits best, chosen by the JEV model (recalculates on every change) |177+| `JEV.PROB(value, question, [yes_means], [no_means])` | The probability the answer is yes, from the JEV model (recalculates on every change) |178+| `JEV.SCORE(value, question, levels)` | A score on an ordered rubric, from the JEV model (recalculates on every change) |179+| `JEV.TEST(value, question, [yes_means], [no_means])` | TRUE or FALSE, answered by the JEV model (recalculates on every change) |180+@@ -0,0 +1,142 @@
1+package sheet2+3+import (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+)16+17+var updateDocs = flag.Bool("update-docs", false, "rewrite docs/functions.md from the function table")18+19+// docsPath is the generated function reference.20+const docsPath = "../../docs/functions.md"21+22+// categories names each functions*.go file's group, in the order the23+// reference lists them.24+var categories = []struct{ file, title string }{25+ {"functions.go", "Everyday"},26+ {"functions_math.go", "Math"},27+ {"functions_stats.go", "Statistics"},28+ {"functions_logic.go", "Logic and information"},29+ {"functions_text.go", "Text"},30+ {"functions_lookup.go", "Lookup"},31+ {"functions_date.go", "Date and time"},32+ {"functions_finance.go", "Finance"},33+ {"functions_link.go", "Links"},34+ {"functions_jev.go", "JEV (hosted model)"},35+}36+37+// TestFunctionsDoc keeps docs/functions.md in step with the function38+// table, as help is: run `go test ./internal/sheet -run FunctionsDoc39+// -update-docs` after adding a function.40+func TestFunctionsDoc(t *testing.T) {41+ byFile, err := definingFiles()42+ if err != nil {43+ t.Fatal(err)44+ }45+ got := functionsDoc(byFile)46+ if *updateDocs {47+ if err := os.WriteFile(docsPath, []byte(got), 0o644); err != nil {48+ t.Fatal(err)49+ }50+ return51+ }52+ want, err := os.ReadFile(docsPath)53+ if err != nil || string(want) != got {54+ t.Errorf("docs/functions.md is stale; run go test ./internal/sheet -run FunctionsDoc -update-docs")55+ }56+}57+58+// definingFiles maps each function name to the file whose FuncDef59+// literal defines it.60+func definingFiles() (map[string]string, error) {61+ fset := gotoken.NewFileSet()62+ out := map[string]string{}63+ files, err := filepath.Glob("functions*.go")64+ if err != nil {65+ return nil, err66+ }67+ for _, path := range files {68+ if strings.HasSuffix(path, "_test.go") {69+ continue70+ }71+ f, err := goparser.ParseFile(fset, path, nil, 0)72+ if err != nil {73+ return nil, err74+ }75+ ast.Inspect(f, func(n ast.Node) bool {76+ kv, ok := n.(*ast.KeyValueExpr)77+ if !ok {78+ return true79+ }80+ if id, ok := kv.Key.(*ast.Ident); ok && id.Name == "Name" {81+ if lit, ok := kv.Value.(*ast.BasicLit); ok && lit.Kind == gotoken.STRING {82+ if name, err := strconv.Unquote(lit.Value); err == nil {83+ out[name] = path84+ }85+ }86+ }87+ return true88+ })89+ }90+ return out, nil91+}92+93+func functionsDoc(byFile map[string]string) string {94+ var b strings.Builder95+ b.WriteString("# Functions\n\n")96+ b.WriteString("<!-- Generated from the engine's function table by TestFunctionsDoc; do not edit. -->\n\n")97+ fmt.Fprintf(&b, "012 has %d functions. They follow Google Sheets' names, arguments and\n", len(Funcs()))98+ b.WriteString("semantics; `[brackets]` mark optional arguments. Function names are\n")99+ b.WriteString("case-insensitive, and 1-2-3's `@SUM(A1..A5)` spelling still works.\n\n")100+ var aliasList []string101+ for a, canon := range aliases {102+ aliasList = append(aliasList, fmt.Sprintf("`%s` for `%s`", a, canon))103+ }104+ slices.Sort(aliasList)105+ if len(aliasList) > 0 {106+ fmt.Fprintf(&b, "Aliases: %s.\n\n", strings.Join(aliasList, ", "))107+ }108+ listed := map[string]bool{}109+ section := func(title string, fns []*FuncDef) {110+ if len(fns) == 0 {111+ return112+ }113+ fmt.Fprintf(&b, "## %s\n\n| Function | Description |\n|---|---|\n", title)114+ for _, f := range fns {115+ sig := f.Name + "(" + f.Args + ")"116+ desc := f.Desc117+ if f.Volatile {118+ desc += " (recalculates on every change)"119+ }120+ fmt.Fprintf(&b, "| `%s` | %s |\n", strings.ReplaceAll(sig, "|", `\|`), desc)121+ listed[f.Name] = true122+ }123+ b.WriteString("\n")124+ }125+ for _, c := range categories {126+ var fns []*FuncDef127+ for _, f := range Funcs() {128+ if byFile[f.Name] == c.file {129+ fns = append(fns, f)130+ }131+ }132+ section(c.title, fns)133+ }134+ var rest []*FuncDef135+ for _, f := range Funcs() {136+ if !listed[f.Name] {137+ rest = append(rest, f)138+ }139+ }140+ section("Other", rest)141+ return b.String()142+}