1use crate::base::date::{DateSystem, MONTHS, serial_from_ymd, time_fraction};
8use crate::base::value::{CellError, Value};
9
10#[derive(Clone, Debug, PartialEq)]
12pub struct Parsed {
13 pub value: Value,
14 pub format: Option<&'static str>,
15}
16
17pub fn parse_input(s: &str, sys: DateSystem) -> Parsed {
20 if s.is_empty() {
21 return Parsed { value: Value::Empty, format: None };
22 }
23 if let Some(rest) = s.strip_prefix('\'') {
24 return Parsed { value: Value::text(rest), format: None };
25 }
26 let t = s.trim();
27 if t.eq_ignore_ascii_case("TRUE") {
28 return Parsed { value: Value::Bool(true), format: None };
29 }
30 if t.eq_ignore_ascii_case("FALSE") {
31 return Parsed { value: Value::Bool(false), format: None };
32 }
33 if let Some(e) = CellError::parse(t) {
34 return Parsed { value: Value::Error(e), format: None };
35 }
36 if let Some((n, f)) = parse_numeric(t, sys) {
37 return Parsed { value: Value::number(n), format: f };
38 }
39 Parsed { value: Value::text(s), format: None }
40}
41
42pub fn parse_number_text(s: &str) -> Option<f64> {
44 parse_number_text_in(s, DateSystem::D1900)
45}
46
47pub fn parse_number_text_in(s: &str, sys: DateSystem) -> Option<f64> {
49 parse_numeric(s.trim(), sys).map(|(n, _)| n)
50}
51
52pub fn number_stored_as_text(s: &str) -> Option<f64> {
56 let t = s.trim();
57 match parenthesized(t) {
59 Some(inner) if inner.strip_prefix('$').map_or(inner.as_str(), str::trim_start).starts_with(['-', '+', '(']) => None,
60 Some(inner) => parse_plain(&inner).map(|(n, _)| -n),
61 None => parse_plain(t).map(|(n, _)| n),
62 }
63}
64
65fn parse_numeric(t: &str, sys: DateSystem) -> Option<(f64, Option<&'static str>)> {
66 if let Some(inner) = parenthesized(t) {
68 if inner.strip_prefix('$').map_or(inner.as_str(), str::trim_start).starts_with(['-', '+', '(']) {
70 return None;
71 }
72 let r = parse_plain(&inner).or_else(|| parse_fraction(&inner).map(|n| (n, Some("# ?/?"))));
73 return r.map(|(n, f)| (-n, f));
74 }
75 parse_unparenthesised(t, sys)
76}
77
78fn parse_unparenthesised(t: &str, sys: DateSystem) -> Option<(f64, Option<&'static str>)> {
79 if t.is_empty() {
80 return None;
81 }
82 if let Some(r) = parse_plain(t) {
83 return Some(r);
84 }
85 if let Some(r) = parse_fraction(t) {
86 return Some((r, Some("# ?/?")));
87 }
88 if let Some(r) = parse_datetime(t, sys) {
89 return Some(r);
90 }
91 None
92}
93
94fn parenthesized(t: &str) -> Option<String> {
98 let (currency, rest) = match t.strip_prefix('$') {
99 Some(r) => ("$", r.trim_start()),
100 None => ("", t),
101 };
102 let rest = rest.strip_prefix('(')?;
103 let (body, percent) = match rest.strip_suffix(')') {
104 Some(b) => (b, ""),
105 None => (rest.strip_suffix('%')?.trim_end().strip_suffix(')')?, "%"),
106 };
107 Some(format!("{currency}{}{percent}", body.trim()))
108}
109
110fn parse_plain(t: &str) -> Option<(f64, Option<&'static str>)> {
112 let mut s = t;
113 let mut neg = false;
114 let mut currency = false;
115 let mut percent = false;
116 if let Some(r) = s.strip_prefix('-') {
117 neg = true;
118 s = r.trim_start();
119 } else if let Some(r) = s.strip_prefix('+') {
120 s = r.trim_start();
121 }
122 if let Some(r) = s.strip_prefix('$') {
123 currency = true;
124 s = r.trim_start();
125 if !neg && let Some(r2) = s.strip_prefix('-') {
126 neg = true;
127 s = r2;
128 }
129 }
130 if let Some(r) = s.strip_suffix('%') {
131 percent = true;
132 s = r.trim_end();
133 }
134 if s.is_empty() || currency && percent {
136 return None;
137 }
138 let has_sep = s.contains(',');
139 let digits: String = if has_sep {
140 let (int, frac) = s.split_once('.').map_or((s, None), |(a, b)| (a, Some(b)));
142 let groups: Vec<&str> = int.split(',').collect();
143 let first = groups.first()?;
144 if first.is_empty() || first.len() > 3 || groups.iter().skip(1).any(|g| g.len() != 3) {
145 return None;
146 }
147 let mut d = groups.concat();
148 if let Some(f) = frac {
149 d.push('.');
150 d.push_str(f);
151 }
152 d
153 } else {
154 s.to_string()
155 };
156 let b = digits.as_bytes();
157 if !b.iter().all(|c| c.is_ascii_digit() || matches!(c, b'.' | b'e' | b'E' | b'+' | b'-')) {
158 return None;
159 }
160 if !b.first().is_some_and(|c| c.is_ascii_digit() || *c == b'.') {
161 return None;
162 }
163 let mut n: f64 = digits.parse().ok()?;
164 if !n.is_finite() {
165 return None;
166 }
167 let sci = digits.contains(['e', 'E']);
168 if percent {
169 n /= 100.0;
170 }
171 if neg {
172 n = -n;
173 }
174 let fmt = if currency {
175 Some(if digits.contains('.') { "\"$\"#,##0.00" } else { "\"$\"#,##0" })
176 } else if percent {
177 Some(if digits.contains('.') { "0.00%" } else { "0%" })
178 } else if sci {
179 Some("0.00E+00")
180 } else if has_sep {
181 Some(if digits.contains('.') { "#,##0.00" } else { "#,##0" })
182 } else {
183 None
184 };
185 Some((n, fmt))
186}
187
188fn plain_number(s: &str, frac: bool) -> Option<f64> {
192 let (int, dec) = match s.split_once('.') {
193 Some((i, d)) if frac => (i, Some(d)),
194 _ => (s, None),
195 };
196 let digits = |x: &str| !x.is_empty() && x.bytes().all(|c| c.is_ascii_digit());
197 if !digits(int) || dec.is_some_and(|d| !digits(d)) {
198 return None;
199 }
200 s.parse().ok().filter(|x: &f64| x.is_finite())
201}
202
203fn parse_fraction(t: &str) -> Option<f64> {
205 let (whole, frac) = t.split_once(' ')?;
206 let neg = whole.starts_with('-');
207 let w = plain_number(whole.trim_start_matches('-'), false)?;
208 let (a, b) = frac.trim().split_once('/')?;
209 let a = plain_number(a, false)?;
210 let b = plain_number(b, false)?;
211 if b == 0.0 || !w.is_finite() {
212 return None;
213 }
214 let v = w + a / b;
215 if !v.is_finite() {
216 return None;
217 }
218 Some(if neg { -v } else { v })
219}
220
221fn month_from_name(s: &str) -> Option<u32> {
222 let l = s.to_ascii_lowercase();
223 if l.len() < 3 {
224 return None;
225 }
226 MONTHS.iter().position(|m| m.to_ascii_lowercase().starts_with(&l)).map(|i| i as u32 + 1)
227}
228
229fn parse_time(t: &str) -> Option<(f64, bool, bool)> {
231 let t = t.trim();
232 let lower = t.to_ascii_lowercase();
233 let (body, ampm) = if let Some(b) = lower.strip_suffix("am").or_else(|| lower.strip_suffix('a')) {
234 (b.trim().to_string(), Some(false))
235 } else if let Some(b) = lower.strip_suffix("pm").or_else(|| lower.strip_suffix('p')) {
236 (b.trim().to_string(), Some(true))
237 } else {
238 (lower.clone(), None)
239 };
240 let parts: Vec<&str> = body.split(':').collect();
241 if parts.len() == 1 && ampm.is_none() {
242 return None;
243 }
244 if parts.len() > 3 || parts.iter().any(|p| p.is_empty() || p.starts_with('+')) {
245 return None;
246 }
247 let mut h = plain_number(parts.first()?, false)?;
248 let m = parts.get(1).map_or(Some(0.0), |p| plain_number(p, false))?;
249 let s = parts.get(2).map_or(Some(0.0), |p| plain_number(p, true))?;
250 if m >= 60.0 || s >= 60.0 || h < 0.0 || m < 0.0 || s < 0.0 {
251 return None;
252 }
253 if let Some(pm) = ampm {
254 if !(0.0..=12.0).contains(&h) {
255 return None;
256 }
257 if h == 12.0 {
258 h = 0.0;
259 }
260 if pm {
261 h += 12.0;
262 }
263 } else if h >= 10000.0 {
264 return None;
265 }
266 Some((time_fraction(h, m, s), parts.len() == 3, ampm.is_some()))
267}
268
269fn parse_date(t: &str, sys: DateSystem) -> Option<(f64, &'static str)> {
270 let t = t.trim().trim_end_matches(',');
271 let num = |s: &str| {
272 let s = s.trim();
273 if s.is_empty() || !s.bytes().all(|b| b.is_ascii_digit()) {
274 return None;
275 }
276 s.parse::<i64>().ok()
277 };
278 let seps: &[char] = &['/', '-', '.'];
280 let parts: Vec<&str> = t.split(seps).collect();
281 if parts.len() == 3 {
282 let (a, b, c) = (parts[0].trim(), parts[1].trim(), parts[2].trim());
283 if a.len() == 4
284 && let (Some(y), Some(m), Some(d)) = (num(a), num(b), num(c))
285 {
286 return valid_ymd(sys, y, m, d).map(|s| (s, "yyyy-mm-dd"));
287 }
288 if let (Some(m), Some(d), Some(y)) = (num(a), num(b), num(c)) {
289 let y = if c.len() <= 2 { if y < 30 { 2000 + y } else { 1900 + y } } else { y };
290 return valid_ymd(sys, y, m, d).map(|s| (s, "m/d/yyyy"));
291 }
292 if let (Some(d), Some(m), Some(y)) = (num(a), month_from_name(b), num(c)) {
294 let y = if c.len() <= 2 { if y < 30 { 2000 + y } else { 1900 + y } } else { y };
295 return valid_ymd(sys, y, m as i64, d).map(|s| (s, "d-mmm-yy"));
296 }
297 }
298 if parts.len() == 2 {
299 let (a, b) = (parts[0].trim(), parts[1].trim());
300 if let (Some(m), Some(d)) = (num(a), num(b)) {
303 let y = current_year();
304 if (1..=12).contains(&m) && (1..=31).contains(&d) {
305 return valid_ymd(sys, y, m, d).map(|s| (s, "d-mmm"));
306 }
307 if (1..=12).contains(&m) && b.len() == 4 {
309 return valid_ymd(sys, d, m, 1).map(|s| (s, "mmm-yy"));
310 }
311 }
312 if let (Some(d), Some(m)) = (num(a), month_from_name(b)) {
313 return valid_ymd(sys, current_year(), m as i64, d).map(|s| (s, "d-mmm"));
314 }
315 if let (Some(m), Some(y)) = (month_from_name(a), num(b)) {
316 let y = if b.len() <= 2 { if y < 30 { 2000 + y } else { 1900 + y } } else { y };
317 return valid_ymd(sys, y, m as i64, 1).map(|s| (s, "mmm-yy"));
318 }
319 }
320 let words: Vec<&str> = t.split([' ', ',']).filter(|w| !w.is_empty()).collect();
322 if words.len() == 3 {
323 let expand = |w: &str, y: i64| if w.len() <= 2 { if y < 30 { 2000 + y } else { 1900 + y } } else { y };
325 if let (Some(m), Some(d), Some(y)) = (month_from_name(words[0]), num(words[1]), num(words[2])) {
326 let y = expand(words[2], y);
327 return valid_ymd(sys, y, m as i64, d).map(|s| (s, "d-mmm-yy"));
328 }
329 if let (Some(d), Some(m), Some(y)) = (num(words[0]), month_from_name(words[1]), num(words[2])) {
330 let y = expand(words[2], y);
331 return valid_ymd(sys, y, m as i64, d).map(|s| (s, "d-mmm-yy"));
332 }
333 }
334 None
335}
336
337fn valid_ymd(sys: DateSystem, y: i64, m: i64, d: i64) -> Option<f64> {
338 if !(1..=12).contains(&m) || d < 1 || y < 1900 {
340 return None;
341 }
342 let max = crate::base::date::days_in_month_in(sys, y as i32, m as u32) as i64;
343 if d > max {
344 return None;
345 }
346 serial_from_ymd(sys, y, m, d)
347}
348
349pub fn current_year() -> i64 {
352 if let Some(ms) = crate::base::date::unix_now_ms() {
353 let days = (ms / 86_400_000.0).floor() as i64;
354 return crate::base::date::civil_from_days(days).0 as i64;
355 }
356 2026
357}
358
359fn parse_datetime(t: &str, sys: DateSystem) -> Option<(f64, Option<&'static str>)> {
360 if let Some((tf, secs, ampm)) = parse_time(t) {
361 let f = match (secs, ampm) {
362 _ if tf >= 1.0 => "[h]:mm:ss",
364 (false, false) => "h:mm",
365 (true, false) => "h:mm:ss",
366 (false, true) => "h:mm AM/PM",
367 (true, true) => "h:mm:ss AM/PM",
368 };
369 return Some((tf, Some(f)));
370 }
371 if let Some((d, f)) = parse_date(t, sys) {
372 return Some((d, Some(f)));
373 }
374 for (i, _) in t.match_indices(' ') {
378 let Some(b) = t.get(i + 1..) else { continue };
379 let bytes = b.as_bytes();
380 if b.len() > 36 || !bytes.first().is_some_and(u8::is_ascii_digit) || !b.contains(':') {
381 continue;
382 }
383 if let (Some((d, _)), Some((tf, _, _))) = (t.get(..i).and_then(|a| parse_date(a, sys)), parse_time(b)) {
384 return Some((d + tf, Some("m/d/yyyy h:mm")));
385 }
386 }
387 None
388}
389
390#[cfg(test)]
391mod tests {
392 use super::*;
393
394 fn p(s: &str) -> Parsed {
395 parse_input(s, DateSystem::D1900)
396 }
397
398 #[test]
400 fn overlong_digits_stay_text() {
401 let big = "9".repeat(400);
402 for s in [format!("1 {big}/{big}"), format!("1 {big}/2"), format!("1 1/{big}0"), format!("{big}:00"), format!("1:00:{big}.5")] {
403 assert_eq!(p(&s).value, Value::text(s.as_str()), "{s}");
404 assert_eq!(parse_number_text(&s), None, "{s}");
405 }
406 assert_eq!(parse_number_text("1 1/2"), Some(1.5));
407 }
408
409 #[test]
410 fn overlong_time_fields_stay_text() {
411 let big = "9".repeat(500);
414 for s in [format!("{big}:00"), format!("1:00:{big}"), format!("{big}:{big}:{big}"), format!("1:0{big}"), format!("00:00:{big}.1")] {
415 assert_eq!(p(&s).value, Value::text(s.as_str()), "{s}");
416 assert_eq!(parse_number_text(&s), None, "{s}");
417 }
418 assert_eq!(p("01:02:03").value, Value::number(3.0 / 86_400.0 + 1.0 / 24.0 + 2.0 / 1440.0));
420 assert_eq!(parse_number_text("13:14:15"), Some(0.5515625));
421 }
422
423 #[test]
424 fn non_digit_time_and_fraction_parts_stay_text() {
425 for s in ["1:nan", "nan:30", "1:30:NaN", "1 nan/2", "1 1/nan", "inf:00", "1e1:00", "+5:00"] {
426 assert_eq!(p(s).value, Value::text(s), "{s}");
427 }
428 }
429
430 #[test]
431 fn signed_date_parts_stay_text() {
432 for s in ["+3/4/2026", "3/+4/2026", "3/4/+2026", "1/1/2020 +5:00"] {
433 assert_eq!(p(s).value, Value::text(s), "{s}");
434 }
435 assert_eq!(p("3/4/2026").value, Value::Number(46085.0));
436 }
437
438 #[test]
439 fn month_name_short_year() {
440 for s in ["Mar 4, 26", "Mar 4 26", "March 4 26", "4 March 26", "Mar 4, 2026"] {
441 assert_eq!(p(s).value, Value::Number(46085.0), "{s}");
442 }
443 assert_eq!(p("Mar 4, 99").value, p("Mar 4, 1999").value);
444 assert_eq!(p("Mar 4, 1776").value, Value::text("Mar 4, 1776"));
445 assert_eq!(p("Feb 30, 26").value, Value::text("Feb 30, 26"));
446 }
447
448 #[test]
449 fn numbers() {
450 assert_eq!(p("42").value, Value::Number(42.0));
451 assert_eq!(p("-3.5").value, Value::Number(-3.5));
452 assert_eq!(p("1,234").format, Some("#,##0"));
453 assert_eq!(p("1,234").value, Value::Number(1234.0));
454 assert_eq!(p("1,23").value, Value::text("1,23"));
455 assert_eq!(p("12%").value, Value::Number(0.12));
456 assert_eq!(p("$1,200.50").value, Value::Number(1200.5));
457 assert_eq!(p("$1,200.50").format, Some("\"$\"#,##0.00"));
458 assert_eq!(p("(100)").value, Value::Number(-100.0));
459 assert_eq!(p("1e3").value, Value::Number(1000.0));
460 assert_eq!(p("1 1/2").value, Value::Number(1.5));
461 assert_eq!(p(".5").value, Value::Number(0.5));
462 }
463
464 #[test]
466 fn accounting_parentheses() {
467 let accepted: [(&str, f64, Option<&str>); 22] = [
468 ("$(5)", -5.0, Some("\"$\"#,##0")),
469 ("$ (5)", -5.0, Some("\"$\"#,##0")),
470 ("$( 5 )", -5.0, Some("\"$\"#,##0")),
471 ("$ ( 1,234 )", -1234.0, Some("\"$\"#,##0")),
472 ("$(1,234)", -1234.0, Some("\"$\"#,##0")),
473 ("$(1,000.50)", -1000.5, Some("\"$\"#,##0.00")),
474 ("$(1,234.)", -1234.0, Some("\"$\"#,##0.00")),
475 ("$(.5)", -0.5, Some("\"$\"#,##0.00")),
476 ("$(0)", 0.0, Some("\"$\"#,##0")),
477 (" $(5) ", -5.0, Some("\"$\"#,##0")),
478 ("($5)", -5.0, Some("\"$\"#,##0")),
479 ("( $ 1,234 )", -1234.0, Some("\"$\"#,##0")),
480 ("(5)", -5.0, None),
481 ("( 5 )", -5.0, None),
482 ("(1,234.5)", -1234.5, Some("#,##0.00")),
483 ("(1e-3)", -0.001, Some("0.00E+00")),
484 ("(5)%", -0.05, Some("0%")),
485 ("( 5 ) %", -0.05, Some("0%")),
486 ("(5%)", -0.05, Some("0%")),
487 ("(5.)%", -0.05, Some("0.00%")),
488 ("(1,234)%", -12.34, Some("0%")),
489 ("(1 1/2)", -1.5, Some("# ?/?")),
490 ];
491 for (s, n, f) in accepted {
492 assert_eq!(p(s), Parsed { value: Value::Number(n), format: f }, "{s}");
493 assert_eq!(parse_number_text(s), Some(n), "{s}");
494 }
495 for s in [
496 "$5%",
497 "$(5%)",
498 "$(5)%",
499 "($5%)",
500 "($5)%",
501 "$ ( 5 ) %",
502 "(5)%%",
503 "(5%)%",
504 "((5))",
505 "(-5)",
506 "(+5)",
507 "($-5)",
508 "($ -5)",
509 "($+5)",
510 "(-$5)",
511 "$(-5)",
512 "$ (-5)",
513 "$(+5)",
514 "-(5)",
515 "-$(5)",
516 "+$(5)",
517 "$+(5)",
518 "$$(5)",
519 "$ $(5)",
520 "$((5))",
521 "($(5))",
522 "(-1 1/2)",
523 "()",
524 "$()",
525 "(5)$",
526 "$(5)$",
527 "(5)(5)",
528 "$(5)0",
529 "(5)e3",
530 "(5)-",
531 "(5-)",
532 "$(5-)",
533 "$(5",
534 "$5)",
535 "(5",
536 "$(1,23)",
537 "$(,5)",
538 "(5e)",
539 "(1/2)",
540 "$(1/2)",
541 "(3/4/2026)",
542 "$(3/4/2026)",
543 "(10:30)",
544 "$(10:30)",
545 ] {
546 assert_eq!(p(s).value, Value::text(s), "{s}");
547 assert_eq!(parse_number_text(s), None, "{s}");
548 }
549 }
550
551 #[test]
552 fn deeply_nested_parentheses_do_not_overflow() {
553 let depth = 100_000;
556 let s = format!("{}1{}", "(".repeat(depth), ")".repeat(depth));
557 assert_eq!(parse_number_text(&s), None);
558 assert_eq!(p(&s).value, Value::text(s.as_str()));
559 let odd = format!("{}1{}", "(".repeat(depth + 1), ")".repeat(depth + 1));
560 assert_eq!(parse_number_text(&odd), None);
561 let unbalanced = format!("{}1{}", "(".repeat(depth), ")".repeat(depth - 1));
562 assert_eq!(parse_number_text(&unbalanced), None);
563 assert_eq!(parse_number_text("(100)"), Some(-100.0));
565 assert_eq!(parse_number_text("( $1,200.50 )"), Some(-1200.5));
566 assert_eq!(parse_number_text("()"), None);
567 }
568
569 #[test]
570 fn texts_and_specials() {
571 assert_eq!(p("hello").value, Value::text("hello"));
572 assert_eq!(p("'123").value, Value::text("123"));
573 assert_eq!(p("true").value, Value::Bool(true));
574 assert_eq!(p("#N/A").value, Value::Error(CellError::NA));
575 assert_eq!(p("e").value, Value::text("e"));
576 assert_eq!(p("-").value, Value::text("-"));
577 assert_eq!(p("1.2.3.4").value, Value::text("1.2.3.4"));
578 }
579
580 #[test]
581 fn dates_and_times() {
582 assert_eq!(p("10/7/2026").value, Value::Number(46302.0));
583 assert_eq!(p("2026-10-07").value, Value::Number(46302.0));
584 assert_eq!(p("7-Oct-2026").value, Value::Number(46302.0));
585 assert_eq!(p("Oct 7, 2026").value, Value::Number(46302.0));
586 assert_eq!(p("2/30/2026").value, Value::text("2/30/2026"));
587 assert_eq!(p("7/4/1776").value, Value::text("7/4/1776"));
588 assert_eq!(p("1776-07-04").value, Value::text("1776-07-04"));
589 assert_eq!(p("4-Jul-1776").value, Value::text("4-Jul-1776"));
590 assert_eq!(p("12/31/1899").value, Value::text("12/31/1899"));
591 assert_eq!(p("1/1/1900").value, Value::Number(1.0));
592 assert_eq!(p("12:00").value, Value::Number(0.5));
593 assert_eq!(p("6:00 PM").value, Value::Number(0.75));
594 assert_eq!(p("12:00 AM").value, Value::Number(0.0));
595 assert_eq!(p("10/7/2026 18:00").value, Value::Number(46302.75));
596 assert_eq!(p("25:00").value, Value::Number(25.0 / 24.0));
597 assert_eq!(p("25:00").format, Some("[h]:mm:ss"));
598 assert_eq!(p("23:59").format, Some("h:mm"));
599 }
600
601 #[test]
602 fn fictitious_1900_02_29() {
603 assert_eq!(p("2/29/1900").value, Value::Number(60.0));
605 assert_eq!(p("1900-02-29").value, Value::Number(60.0));
606 assert_eq!(p("29-Feb-1900").value, Value::Number(60.0));
607 assert_eq!(p("2/28/1900").value, Value::Number(59.0));
608 assert_eq!(p("3/1/1900").value, Value::Number(61.0));
609 assert_eq!(p("2/29/1901").value, Value::text("2/29/1901"));
610 assert_eq!(p("2/30/1900").value, Value::text("2/30/1900"));
611 assert_eq!(parse_input("2/29/1900", DateSystem::D1904).value, Value::text("2/29/1900"));
613 assert_eq!(parse_input("2/29/1904", DateSystem::D1904).value, Value::Number(59.0));
614 }
615
616 #[test]
617 fn numbers_stored_as_text() {
618 for (text, n) in [
619 ("123", 123.0),
620 (" 42 ", 42.0),
621 ("-1,000.5", -1000.5),
622 ("$3", 3.0),
623 ("50%", 0.5),
624 ("1E3", 1000.0),
625 ("(100)", -100.0),
626 ("$(5)", -5.0),
627 ("(5)%", -0.05),
628 ] {
629 assert_eq!(number_stored_as_text(text), Some(n), "{text}");
630 }
631 for text in ["3/4/2026", "10:30", "1 1/2", "abc", "12abc", "", " ", "TRUE", "#N/A", "((1))", "(-5)", "$(5)%", "(1/2)"] {
633 assert_eq!(number_stored_as_text(text), None, "{text}");
634 }
635 }
636}