1use super::decimal::signed_digits_to_decimal;
4use super::{CoreScalarType, ScalarType, ScalarValue};
5
6fn core_tag_type(tag: &str) -> Option<CoreScalarType> {
8 match crate::tag::yaml_org_name(tag)? {
9 "int" => Some(CoreScalarType::Integer),
10 "float" => Some(CoreScalarType::Float),
11 "bool" => Some(CoreScalarType::Boolean),
12 "null" => Some(CoreScalarType::Null),
13 _ => None,
14 }
15}
16
17fn core_tag_accepts(t: CoreScalarType, value: &str) -> bool {
19 match (t, ScalarValue::classify_plain(value)) {
20 (CoreScalarType::Null, _) if value.is_empty() => true,
21 (CoreScalarType::Float, CoreScalarType::Integer) => true,
23 (t, c) => t == c,
24 }
25}
26
27fn take_number(rest: &mut &[char], min: usize, max: usize) -> Option<(u32, usize)> {
30 let len = rest
31 .iter()
32 .take(max)
33 .take_while(|c| c.is_ascii_digit())
34 .count();
35 if len < min {
36 return None;
37 }
38 let value = rest[..len].iter().collect::<String>().parse().ok()?;
39 *rest = &rest[len..];
40 Some((value, len))
41}
42
43fn take_char(rest: &mut &[char], accept: impl Fn(char) -> bool) -> Option<char> {
45 let (&c, tail) = rest.split_first().filter(|(&c, _)| accept(c))?;
46 *rest = tail;
47 Some(c)
48}
49
50pub(super) fn all_digits(s: &str, radix: u32) -> bool {
51 !s.is_empty() && s.chars().all(|c| c.is_digit(radix))
52}
53
54impl ScalarValue {
55 pub(crate) fn parse_integer(value: &str) -> Option<i64> {
58 let (radix, digits, is_negative) = Self::split_core_int(value)?;
59 let magnitude = i128::from_str_radix(digits, radix).ok()?;
61 i64::try_from(if is_negative { -magnitude } else { magnitude }).ok()
62 }
63
64 pub(crate) fn canonical_integer(value: &str) -> Option<String> {
68 let (radix, digits, is_negative) = Self::split_core_int(value)?;
69 Some(signed_digits_to_decimal(digits, radix, is_negative))
70 }
71
72 fn split_core_int(value: &str) -> Option<(u32, &str, bool)> {
75 if !Self::is_core_int(value) {
76 return None;
77 }
78 Some(if let Some(hex) = value.strip_prefix("0x") {
79 (16, hex, false)
80 } else if let Some(oct) = value.strip_prefix("0o") {
81 (8, oct, false)
82 } else if let Some(dec) = value.strip_prefix('-') {
83 (10, dec, true)
84 } else {
85 (10, value.strip_prefix('+').unwrap_or(value), false)
86 })
87 }
88
89 pub(crate) fn parse_float(value: &str) -> Option<f64> {
91 match value {
92 ".inf" | ".Inf" | ".INF" | "+.inf" | "+.Inf" | "+.INF" => Some(f64::INFINITY),
93 "-.inf" | "-.Inf" | "-.INF" => Some(f64::NEG_INFINITY),
94 ".nan" | ".NaN" | ".NAN" => Some(f64::NAN),
95 _ if Self::is_core_float(value) => value.parse::<f64>().ok(),
96 _ => None,
97 }
98 }
99
100 pub(crate) fn parse_bool(value: &str) -> Option<bool> {
102 match value {
103 "true" | "True" | "TRUE" => Some(true),
104 "false" | "False" | "FALSE" => Some(false),
105 _ => None,
106 }
107 }
108
109 fn is_core_int(value: &str) -> bool {
115 if let Some(hex) = value.strip_prefix("0x") {
116 return all_digits(hex, 16);
117 }
118 if let Some(oct) = value.strip_prefix("0o") {
119 return all_digits(oct, 8);
120 }
121 all_digits(value.strip_prefix(['+', '-']).unwrap_or(value), 10)
122 }
123
124 pub(crate) fn is_core_float(value: &str) -> bool {
128 let body = value.strip_prefix(['+', '-']).unwrap_or(value);
129 let (mantissa, exponent) = match body.find(['e', 'E']) {
130 Some(i) => (&body[..i], Some(&body[i + 1..])),
131 None => (body, None),
132 };
133 let mantissa_ok = match mantissa.split_once('.') {
134 Some(("", frac)) => all_digits(frac, 10),
135 Some((int, frac)) => all_digits(int, 10) && frac.chars().all(|c| c.is_ascii_digit()),
136 None => all_digits(mantissa, 10),
137 };
138 let exponent_ok = match exponent {
139 Some(e) => all_digits(e.strip_prefix(['+', '-']).unwrap_or(e), 10),
140 None => true,
141 };
142 mantissa_ok && exponent_ok
143 }
144
145 pub(crate) fn is_legacy_octal(value: &str) -> bool {
151 let body = value.strip_prefix(['+', '-']).unwrap_or(value);
152 body.len() > 1 && body.starts_with('0') && all_digits(body, 8)
153 }
154
155 pub(crate) fn is_leading_zero_decimal(value: &str) -> bool {
160 let body = value.strip_prefix(['+', '-']).unwrap_or(value);
161 body.len() > 1 && body.starts_with('0') && all_digits(body, 10) && !all_digits(body, 8)
162 }
163
164 pub fn classify_plain(value: &str) -> CoreScalarType {
175 match value {
176 _ if Self::parse_bool(value).is_some() => return CoreScalarType::Boolean,
177 "null" | "Null" | "NULL" | "~" | "" => return CoreScalarType::Null,
178 _ => {}
179 }
180
181 if Self::is_core_int(value) {
182 return CoreScalarType::Integer;
183 }
184
185 if Self::parse_float(value).is_some() {
188 return CoreScalarType::Float;
189 }
190
191 CoreScalarType::String
192 }
193
194 pub(crate) fn format_float(display: String) -> String {
199 match display.as_str() {
200 "NaN" => ".nan".to_string(),
201 "inf" => ".inf".to_string(),
202 "-inf" => "-.inf".to_string(),
203 _ if display.contains(['.', 'e', 'E']) => display,
204 _ => format!("{display}.0"),
205 }
206 }
207
208 pub fn auto_detect_type(value: &str) -> ScalarType {
213 match Self::classify_plain(value) {
214 CoreScalarType::String if Self::is_valid_timestamp(value) => ScalarType::Timestamp,
215 other => other.into(),
216 }
217 }
218
219 pub(crate) fn from_tagged(tagged: &crate::yaml::TaggedNode) -> Option<Self> {
226 let scalar = Self::from_scalar(&tagged.value()?);
227 Some(match tagged.tag() {
228 Some(tag) => scalar.with_resolved_tag(tag),
229 None => Self {
230 scalar_type: ScalarType::String,
231 ..scalar
232 },
233 })
234 }
235
236 pub(crate) fn with_resolved_tag(self, tag: String) -> Self {
240 let scalar_type = match core_tag_type(&tag) {
241 Some(t) if core_tag_accepts(t, &self.value) => t.into(),
242 _ => ScalarType::String,
243 };
244 Self {
245 scalar_type,
246 tag: Some(tag),
247 ..self
248 }
249 }
250
251 pub(crate) fn with_type(self, scalar_type: ScalarType) -> Self {
253 Self {
254 scalar_type,
255 ..self
256 }
257 }
258
259 pub(super) fn is_valid_timestamp(value: &str) -> bool {
261 Self::matches_iso8601_pattern(value)
265 }
266
267 fn matches_iso8601_pattern(value: &str) -> bool {
272 let chars: Vec<char> = value.chars().collect();
273 let rest = &mut &chars[..];
274 let Some(((year, _), (month, month_len), (day, day_len))) = (|| {
275 let year = take_number(rest, 4, 4)?;
276 take_char(rest, |c| c == '-')?;
277 let month = take_number(rest, 1, 2)?;
278 take_char(rest, |c| c == '-')?;
279 Some((year, month, take_number(rest, 1, 2)?))
280 })() else {
281 return false;
282 };
283 let leap = year % 4 == 0 && (year % 100 != 0 || year % 400 == 0);
284 let days_in_month = match month {
285 2 if leap => 29,
286 2 => 28,
287 4 | 6 | 9 | 11 => 30,
288 _ => 31,
289 };
290 if !(1..=12).contains(&month) || !(1..=days_in_month).contains(&day) {
291 return false;
292 }
293 if rest.is_empty() {
295 return month_len == 2 && day_len == 2;
296 }
297
298 let time = (|| {
299 if take_char(rest, |c| c == 'T' || c == 't').is_none() {
300 take_char(rest, |c| c == ' ' || c == '\t')?;
301 while take_char(rest, |c| c == ' ' || c == '\t').is_some() {}
302 }
303 let (hour, _) = take_number(rest, 1, 2)?;
304 take_char(rest, |c| c == ':')?;
305 let (minute, _) = take_number(rest, 2, 2)?;
306 take_char(rest, |c| c == ':')?;
307 Some((hour, minute, take_number(rest, 2, 2)?.0))
308 })();
309 match time {
310 Some((hour, minute, second)) if hour <= 23 && minute <= 59 && second <= 59 => {
311 Self::is_timestamp_tail(rest)
312 }
313 _ => false,
314 }
315 }
316
317 fn is_timestamp_tail(tail: &[char]) -> bool {
320 let digits = |s: &[char]| s.iter().all(|c| c.is_ascii_digit());
321 let mut rest = tail;
322 if let Some(('.', fraction)) = rest.split_first() {
323 rest = &fraction[fraction.iter().take_while(|c| c.is_ascii_digit()).count()..];
324 }
325 if rest.is_empty() {
326 return true;
327 }
328 let zone = &rest[rest.iter().take_while(|c| matches!(c, ' ' | '\t')).count()..];
329 match zone {
330 ['Z'] => true,
331 ['+' | '-', offset @ ..] => {
332 let (hour, minute) = match offset.iter().position(|&c| c == ':') {
333 Some(i) => (&offset[..i], &offset[i + 1..]),
334 None => (offset, &['0', '0'][..]),
335 };
336 matches!(hour.len(), 1 | 2) && digits(hour) && minute.len() == 2 && digits(minute)
337 }
338 _ => false,
339 }
340 }
341}
342
343#[cfg(test)]
344mod tests {
345 use super::*;
346
347 #[test]
348 fn test_timestamp_pattern_matching() {
349 assert!(ScalarValue::matches_iso8601_pattern("2023-12-25"));
351 assert!(ScalarValue::matches_iso8601_pattern("2023-12-25T10:30:45"));
352 assert!(ScalarValue::matches_iso8601_pattern("2023-12-25t10:30:45")); assert!(ScalarValue::matches_iso8601_pattern("2023-12-25 10:30:45"));
354 assert!(ScalarValue::matches_iso8601_pattern("2023-01-01T00:00:00"));
355 assert!(ScalarValue::matches_iso8601_pattern(
356 "2001-12-14t21:59:43.10-05:00"
357 )); assert!(!ScalarValue::matches_iso8601_pattern("2023-13-25")); assert!(!ScalarValue::matches_iso8601_pattern("23-12-25")); assert!(!ScalarValue::matches_iso8601_pattern("2023/12/25")); assert!(!ScalarValue::matches_iso8601_pattern("not-a-date"));
364 assert!(!ScalarValue::matches_iso8601_pattern("2023"));
365 }
366
367 #[test]
368 fn test_unix_timestamp_bounds() {
369 assert!(!ScalarValue::is_valid_timestamp("0"));
371 assert!(!ScalarValue::is_valid_timestamp("1"));
372 assert!(!ScalarValue::is_valid_timestamp("4102444799"));
373 assert!(!ScalarValue::is_valid_timestamp("4102444800"));
374 assert!(ScalarValue::is_valid_timestamp("2001-12-14"));
375
376 assert!(ScalarValue::string("1")
378 .coerce_to_type(ScalarType::Timestamp)
379 .is_none());
380 assert_eq!(
381 ScalarValue::from(1_700_000_000i64).coerce_to_type(ScalarType::Timestamp),
382 None
383 );
384 assert_eq!(
385 ScalarValue::string("2001-12-14").coerce_to_type(ScalarType::Timestamp),
386 Some(ScalarValue::timestamp("2001-12-14"))
387 );
388 }
389
390 #[test]
391 fn test_iso8601_time_component_bounds() {
392 assert!(ScalarValue::matches_iso8601_pattern("2023-01-01T23:59:59"));
394 assert!(!ScalarValue::matches_iso8601_pattern("2023-01-01T24:00:00"));
395 assert!(!ScalarValue::matches_iso8601_pattern("2023-01-01T10:60:00"));
396 assert!(!ScalarValue::matches_iso8601_pattern("2023-01-01T10:30:60"));
397
398 assert!(ScalarValue::matches_iso8601_pattern("2023-01-31"));
400 assert!(!ScalarValue::matches_iso8601_pattern("2023-01-00"));
401 assert!(!ScalarValue::matches_iso8601_pattern("2023-01-32"));
402
403 assert!(!ScalarValue::matches_iso8601_pattern("2023-01-01X10:30:45"));
405 assert!(!ScalarValue::matches_iso8601_pattern("2023-01-01T10:30:xx"));
407
408 assert!(!ScalarValue::matches_iso8601_pattern("2023X12-25"));
412 assert!(!ScalarValue::matches_iso8601_pattern("2023-12X25"));
413 }
414
415 #[test]
416 fn test_timestamp_rejects_impossible_dates_and_trailing_text() {
417 for text in [
418 "2023-02-30",
419 "2023-02-29",
420 "2023-04-31",
421 "2100-02-29",
422 "2023-01-01T10:00:00garbage",
423 "2023-01-01T10:00:00Zx",
424 "2023-01-01T10:00:00+",
425 "2023-01-01T10:00:00+01:0",
426 ] {
427 assert!(!ScalarValue::matches_iso8601_pattern(text), "{text}");
428 assert_eq!(
429 ScalarValue::auto_detect_type(text),
430 ScalarType::String,
431 "{text}"
432 );
433 }
434 for text in [
435 "2024-02-29",
436 "2000-02-29",
437 "2023-01-01T10:00:00Z",
438 "2023-01-01T10:00:00.123456Z",
439 "2023-01-01T10:00:00.5+01:00",
440 "2001-12-14 21:59:43.10 -5",
441 "2001-12-14t21:59:43.10-05:00",
442 ] {
443 assert!(ScalarValue::matches_iso8601_pattern(text), "{text}");
444 }
445 }
446}