1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
use crate;
/// `MAX(value1, ...)` — largest numeric value in the arguments.
/// Direct args: Numbers, Dates, Bool (TRUE=1, FALSE=0), parseable text coerced
/// to number.
/// Array elements: Numbers and Dates; text/Bool → skip; errors propagate.
///
/// "No numbers" is *two* rules, not one:
///
/// - an **empty** array argument is `#REF!`: `=MAX({})` — the one rule with
/// an in-repo row (statistical.tsv);
/// - a **populated** array holding nothing numeric is 0: `=MAX({"a","b"})`
/// and `=MAX({TRUE,FALSE})` are both 0, even though neither text nor
/// booleans contribute a number in array context. Captured in Google
/// Sheets; the rows land separately, since they fail until this code exists.
///
/// An array of nothing but *blanks* — what `=MAX(A1:A3)` over an untouched
/// column materializes as — is 0 as well. That is captured across seven range
/// shapes, each with a populated control; the shapes, the controls and where
/// the rows live are set out on [`stat_helpers::is_blank_only_array`], which
/// is what decides the case. Deciding it there rather than through
/// `array_had_content` is deliberate: a blank sitting next to something else
/// numberless changes nothing.
///
/// `array_had_content` is still set by text and booleans alone, so it exempts
/// exactly what those captures cover and makes no claim past them. A zoned
/// instant is the remaining numberless case: unprobed, and still `#REF!`.
///
/// [`stat_helpers::is_blank_only_array`]: super::stat_helpers::is_blank_only_array
///
/// **Dates participate as bare serials** and carry their type out. A date-only
/// range answers the latest date, a date beside a plain number is compared on
/// the serial with no special casing, and the result is date-typed whenever a
/// date took part — even when a plain number won the comparison.
///
/// Captured and extrapolated are not the same thing here, so keep them apart:
///
/// - **Values** are captured in Google Sheets for every form — a date-only
/// column, a date/number column, array literals of both shapes, and dates
/// passed as direct arguments.
/// - **Typing** is captured for the *range* forms only, read back through the
/// cell that holds the result (`=MAX(<date-only range>)` and
/// `=MAX(<date/number range>)` both come back `date`). The literal and
/// direct-argument rows report `number`, but that is an artifact of the
/// capture harness reading them through an `INDEX(...,1,1)` wrapper, which
/// drops the cell's date format — it is not a Sheets answer. So the date
/// typing of `=MAX({DATE(...),DATE(...)})` is **extrapolated** from the
/// range forms, not probed.
///
/// **None of those rows are in this repo yet.** They come off the
/// conformance-fixtures pipeline and land in a separate fixtures-only PR —
/// they fail until this code exists, and CI rejects a PR that mixes fixture
/// TSVs with code. Same arrangement as the blank-only rows described on
/// `stat_helpers::is_blank_only_array`. A reviewer working from this repo
/// alone can check the unit tests and this comment; the Sheets answers
/// themselves have to be taken from that pipeline.
///
/// Two consequences were filed separately and are now closed:
///
/// - `COUNT` did not count dates, so `=COUNT(MAX(<date range>))` answered 0
/// where it used to answer 1. Fixed in `count` against captured Sheets rows
/// (#780).
/// - `MAXA`/`MINA` silently dropped a `Zoned` sitting beside a `Date`, where
/// `MAX`/`MIN` route the same input through `zoned_extreme` and error. The
/// A-variants now consult the same helper, so all four agree; the rule is a
/// deliberate truecalc-only decision recorded on `maxa_fn` (#781).
/// Recursively fold a nested array's numbers into `result` for MAX's
/// array-context rules (Bool/Text/Empty skipped, errors propagate).
/// A sparkline is skipped wherever it appears, and an aggregate whose scope
/// holds nothing else answers 0 — the same answer whether it arrived as a
/// direct argument or through a range (google.tsv: `=MAX(SPARKLINE({1,2,3}))`
/// and `=MAX(Data!K1:K1)` are both 0). The flag is what distinguishes "skipped a
/// sparkline" from "saw nothing usable at all", which stay different answers.
///
/// A `Date` folds in as its bare serial and raises `saw_date`, which types the
/// answer; it never touches `had_content`, since a date always leaves a number
/// behind and so can never reach the numberless rule.
///
/// `had_content` is set by text and booleans *only* — the two variants the
/// capture covers (`=MAX({"a","b"})` and `=MAX({TRUE,FALSE})` are both 0).
/// It is deliberately not a catch-all: every other non-numeric variant leaves
/// it alone and so keeps the `#REF!` MAX has always answered. Listing the
/// variants rather than falling through also stops a future `Value` kind
/// inheriting content-hood by accident. Two variants no longer end at `#REF!`,
/// and neither gets there through this flag: an all-blank array is decided by
/// the blank-only check in `max_fn`, and a `Date` contributes a number so the
/// numberless rule is never reached at all.