From 883a07fb7bf6ac5c8354d2c25a3d97b7d5374823 Mon Sep 17 00:00:00 2001 From: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Date: Thu, 15 Oct 2026 17:31:12 +0200 Subject: [PATCH] Keep nan/inf time and fraction look-alikes as text Found while testing crates/core/src/parse.rs:~170 (parse_time, parts parsed with f64::parse) Co-Authored-By: Claude Haiku 5.5 Co-Authored-By: Claude Sonnet 5.5 --- crates/core/src/parse.rs | 33 +++++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/crates/core/src/parse.rs b/crates/core/src/parse.rs index b465beaf9..f525b7966 100644 --- a/crates/core/src/parse.rs +++ b/crates/core/src/parse.rs @@ -146,14 +146,28 @@ fn parse_plain(t: &str) -> Option<(f64, Option<&'static str>)> { Some((n, fmt)) } +/// A plain ASCII number: digits, with an optional `.digits` fraction when `frac` is set. Rejects +/// `nan`, `inf`, signs and exponents that `f64::from_str` would accept. +fn plain_number(s: &str, frac: bool) -> Option { + let (int, dec) = match s.split_once('.') { + Some((i, d)) if frac => (i, Some(d)), + _ => (s, None), + }; + let digits = |x: &str| !x.is_empty() && x.bytes().all(|c| c.is_ascii_digit()); + if !digits(int) || dec.is_some_and(|d| !digits(d)) { + return None; + } + s.parse().ok() +} + /// `1/2` (only with a whole part, else it's a date), `1 1/2`, `0 3/4`. fn parse_fraction(t: &str) -> Option { let (whole, frac) = t.split_once(' ')?; let neg = whole.starts_with('-'); - let w: f64 = whole.trim_start_matches('-').parse().ok()?; + let w = plain_number(whole.trim_start_matches('-'), false)?; let (a, b) = frac.trim().split_once('/')?; - let a: f64 = a.parse().ok()?; - let b: f64 = b.parse().ok()?; + let a = plain_number(a, false)?; + let b = plain_number(b, false)?; if b == 0.0 || !w.is_finite() { return None; } @@ -187,9 +201,9 @@ fn parse_time(t: &str) -> Option<(f64, bool, bool)> { if parts.len() > 3 || parts.iter().any(|p| p.is_empty()) { return None; } - let mut h: f64 = parts.first()?.parse().ok()?; - let m: f64 = parts.get(1).map_or(Some(0.0), |p| p.parse().ok())?; - let s: f64 = parts.get(2).map_or(Some(0.0), |p| p.parse().ok())?; + let mut h = plain_number(parts.first()?, false)?; + let m = parts.get(1).map_or(Some(0.0), |p| plain_number(p, false))?; + let s = parts.get(2).map_or(Some(0.0), |p| plain_number(p, true))?; if m >= 60.0 || s >= 60.0 || h < 0.0 || m < 0.0 || s < 0.0 { return None; } @@ -326,6 +340,13 @@ mod tests { parse_input(s, DateSystem::D1900) } + #[test] + fn non_digit_time_and_fraction_parts_stay_text() { + for s in ["1:nan", "nan:30", "1:30:NaN", "1 nan/2", "1 1/nan", "inf:00", "1e1:00", "+5:00"] { + assert_eq!(p(s).value, Value::text(s), "{s}"); + } + } + #[test] fn month_name_short_year() { for s in ["Mar 4, 26", "Mar 4 26", "March 4 26", "4 March 26", "Mar 4, 2026"] {