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) => ScalarValue::is_core_float(value),
24 (t, c) => t == c,
25 }
26}
27
28fn take_number(rest: &mut &[char], min: usize, max: usize) -> Option<(u32, usize)> {
31 let len = rest
32 .iter()
33 .take(max)
34 .take_while(|c| c.is_ascii_digit())
35 .count();
36 if len < min {
37 return None;
38 }
39 let value = rest[..len].iter().collect::<String>().parse().ok()?;
40 *rest = &rest[len..];
41 Some((value, len))
42}
43
44fn take_char(rest: &mut &[char], accept: impl Fn(char) -> bool) -> Option<char> {
46 let (&c, tail) = rest.split_first().filter(|(&c, _)| accept(c))?;
47 *rest = tail;
48 Some(c)
49}
50
51pub(super) fn all_digits(s: &str, radix: u32) -> bool {
52 !s.is_empty() && s.chars().all(|c| c.is_digit(radix))
53}
54
55impl ScalarValue {
56 pub(crate) fn parse_integer(value: &str) -> Option<i64> {
59 let (radix, digits, is_negative) = Self::split_core_int(value)?;
60 let magnitude = i128::from_str_radix(digits, radix).ok()?;
62 i64::try_from(if is_negative { -magnitude } else { magnitude }).ok()
63 }
64
65 pub(crate) fn canonical_integer(value: &str) -> Option<String> {
69 let (radix, digits, is_negative) = Self::split_core_int(value)?;
70 Some(signed_digits_to_decimal(digits, radix, is_negative))
71 }
72
73 fn split_core_int(value: &str) -> Option<(u32, &str, bool)> {
76 if !Self::is_core_int(value) {
77 return None;
78 }
79 Some(if let Some(hex) = value.strip_prefix("0x") {
80 (16, hex, false)
81 } else if let Some(oct) = value.strip_prefix("0o") {
82 (8, oct, false)
83 } else if let Some(dec) = value.strip_prefix('-') {
84 (10, dec, true)
85 } else {
86 (10, value.strip_prefix('+').unwrap_or(value), false)
87 })
88 }
89
90 pub(crate) fn parse_float(value: &str) -> Option<f64> {
92 match value {
93 ".inf" | ".Inf" | ".INF" | "+.inf" | "+.Inf" | "+.INF" => Some(f64::INFINITY),
94 "-.inf" | "-.Inf" | "-.INF" => Some(f64::NEG_INFINITY),
95 ".nan" | ".NaN" | ".NAN" => Some(f64::NAN),
96 _ if Self::is_core_float(value) => value.parse::<f64>().ok(),
97 _ => None,
98 }
99 }
100
101 pub(crate) fn parse_bool(value: &str) -> Option<bool> {
103 match value {
104 "true" | "True" | "TRUE" => Some(true),
105 "false" | "False" | "FALSE" => Some(false),
106 _ => None,
107 }
108 }
109
110 fn is_core_int(value: &str) -> bool {
116 if let Some(hex) = value.strip_prefix("0x") {
117 return all_digits(hex, 16);
118 }
119 if let Some(oct) = value.strip_prefix("0o") {
120 return all_digits(oct, 8);
121 }
122 all_digits(value.strip_prefix(['+', '-']).unwrap_or(value), 10)
123 }
124
125 pub(crate) fn is_core_float(value: &str) -> bool {
129 let body = value.strip_prefix(['+', '-']).unwrap_or(value);
130 let (mantissa, exponent) = match body.find(['e', 'E']) {
131 Some(i) => (&body[..i], Some(&body[i + 1..])),
132 None => (body, None),
133 };
134 let mantissa_ok = match mantissa.split_once('.') {
135 Some(("", frac)) => all_digits(frac, 10),
136 Some((int, frac)) => all_digits(int, 10) && frac.chars().all(|c| c.is_ascii_digit()),
137 None => all_digits(mantissa, 10),
138 };
139 let exponent_ok = match exponent {
140 Some(e) => all_digits(e.strip_prefix(['+', '-']).unwrap_or(e), 10),
141 None => true,
142 };
143 mantissa_ok && exponent_ok
144 }
145
146 pub(crate) fn is_legacy_octal(value: &str) -> bool {
152 let body = value.strip_prefix(['+', '-']).unwrap_or(value);
153 body.len() > 1 && body.starts_with('0') && all_digits(body, 8)
154 }
155
156 pub(crate) fn is_leading_zero_decimal(value: &str) -> bool {
161 let body = value.strip_prefix(['+', '-']).unwrap_or(value);
162 body.len() > 1 && body.starts_with('0') && all_digits(body, 10) && !all_digits(body, 8)
163 }
164
165 pub fn classify_plain(value: &str) -> CoreScalarType {
176 match value {
177 _ if Self::parse_bool(value).is_some() => return CoreScalarType::Boolean,
178 "null" | "Null" | "NULL" | "~" | "" => return CoreScalarType::Null,
179 _ => {}
180 }
181
182 if Self::is_core_int(value) {
183 return CoreScalarType::Integer;
184 }
185
186 if Self::parse_float(value).is_some() {
189 return CoreScalarType::Float;
190 }
191
192 CoreScalarType::String
193 }
194
195 pub(crate) fn format_float(display: String) -> String {
200 match display.as_str() {
201 "NaN" => ".nan".to_string(),
202 "inf" => ".inf".to_string(),
203 "-inf" => "-.inf".to_string(),
204 _ if display.contains(['.', 'e', 'E']) => display,
205 _ => format!("{display}.0"),
206 }
207 }
208
209 pub fn auto_detect_type(value: &str) -> ScalarType {
214 match Self::classify_plain(value) {
215 CoreScalarType::String if Self::is_valid_timestamp(value) => ScalarType::Timestamp,
216 other => other.into(),
217 }
218 }
219
220 pub(crate) fn core_tag_scalar_type(tag: &str) -> Option<ScalarType> {
223 core_tag_type(tag).map(Into::into)
224 }
225
226 pub(crate) fn from_tagged(tagged: &crate::yaml::TaggedNode) -> Option<Self> {
235 let scalar = Self::from_scalar(&tagged.value()?);
236 Some(match (tagged.tag(), tagged.resolved_tag()) {
237 (Some(tag), Some(resolved)) => scalar.with_tag_resolved_as(tag, &resolved),
238 _ => Self {
239 scalar_type: ScalarType::String,
240 ..scalar
241 },
242 })
243 }
244
245 pub(crate) fn with_resolved_tag(self, tag: String) -> Self {
249 let resolved = crate::tag::normalize(&tag).into_owned();
250 self.with_tag_resolved_as(tag, &resolved)
251 }
252
253 pub(crate) fn with_tag_resolved_as(self, tag: String, resolved: &str) -> Self {
256 let scalar_type = match core_tag_type(resolved) {
257 Some(t) if core_tag_accepts(t, &self.value) => t.into(),
258 Some(_) => ScalarType::String,
259 None => match resolved {
260 crate::tag::TIMESTAMP => ScalarType::Timestamp,
261 crate::tag::REGEX => ScalarType::Regex,
262 #[cfg(feature = "base64")]
263 crate::tag::BINARY => ScalarType::Binary,
264 _ => ScalarType::String,
265 },
266 };
267 Self {
268 scalar_type,
269 tag: Some(tag),
270 ..self
271 }
272 }
273
274 pub(crate) fn with_type(self, scalar_type: ScalarType) -> Self {
276 Self {
277 scalar_type,
278 ..self
279 }
280 }
281
282 pub(super) fn is_valid_timestamp(value: &str) -> bool {
284 Self::matches_iso8601_pattern(value)
288 }
289
290 fn matches_iso8601_pattern(value: &str) -> bool {
295 let chars: Vec<char> = value.chars().collect();
296 let rest = &mut &chars[..];
297 let Some(((year, _), (month, month_len), (day, day_len))) = (|| {
298 let year = take_number(rest, 4, 4)?;
299 take_char(rest, |c| c == '-')?;
300 let month = take_number(rest, 1, 2)?;
301 take_char(rest, |c| c == '-')?;
302 Some((year, month, take_number(rest, 1, 2)?))
303 })() else {
304 return false;
305 };
306 let leap = year % 4 == 0 && (year % 100 != 0 || year % 400 == 0);
307 let days_in_month = match month {
308 2 if leap => 29,
309 2 => 28,
310 4 | 6 | 9 | 11 => 30,
311 _ => 31,
312 };
313 if !(1..=12).contains(&month) || !(1..=days_in_month).contains(&day) {
314 return false;
315 }
316 if rest.is_empty() {
318 return month_len == 2 && day_len == 2;
319 }
320
321 let time = (|| {
322 if take_char(rest, |c| c == 'T' || c == 't').is_none() {
323 take_char(rest, |c| c == ' ' || c == '\t')?;
324 while take_char(rest, |c| c == ' ' || c == '\t').is_some() {}
325 }
326 let (hour, _) = take_number(rest, 1, 2)?;
327 take_char(rest, |c| c == ':')?;
328 let (minute, _) = take_number(rest, 2, 2)?;
329 take_char(rest, |c| c == ':')?;
330 Some((hour, minute, take_number(rest, 2, 2)?.0))
331 })();
332 match time {
333 Some((hour, minute, second)) if hour <= 23 && minute <= 59 && second <= 59 => {
334 Self::is_timestamp_tail(rest)
335 }
336 _ => false,
337 }
338 }
339
340 fn is_timestamp_tail(tail: &[char]) -> bool {
343 let digits = |s: &[char]| s.iter().all(|c| c.is_ascii_digit());
344 let mut rest = tail;
345 if let Some(('.', fraction)) = rest.split_first() {
346 rest = &fraction[fraction.iter().take_while(|c| c.is_ascii_digit()).count()..];
347 }
348 if rest.is_empty() {
349 return true;
350 }
351 let zone = &rest[rest.iter().take_while(|c| matches!(c, ' ' | '\t')).count()..];
352 match zone {
353 ['Z'] => true,
354 ['+' | '-', offset @ ..] => {
355 let (hour, minute) = match offset.iter().position(|&c| c == ':') {
356 Some(i) => (&offset[..i], &offset[i + 1..]),
357 None => (offset, &['0', '0'][..]),
358 };
359 matches!(hour.len(), 1 | 2) && digits(hour) && minute.len() == 2 && digits(minute)
360 }
361 _ => false,
362 }
363 }
364}
365
366#[cfg(test)]
367mod tests {
368 use super::*;
369
370 #[test]
371 fn test_timestamp_pattern_matching() {
372 assert!(ScalarValue::matches_iso8601_pattern("2023-12-25"));
374 assert!(ScalarValue::matches_iso8601_pattern("2023-12-25T10:30:45"));
375 assert!(ScalarValue::matches_iso8601_pattern("2023-12-25t10:30:45")); assert!(ScalarValue::matches_iso8601_pattern("2023-12-25 10:30:45"));
377 assert!(ScalarValue::matches_iso8601_pattern("2023-01-01T00:00:00"));
378 assert!(ScalarValue::matches_iso8601_pattern(
379 "2001-12-14t21:59:43.10-05:00"
380 )); 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"));
387 assert!(!ScalarValue::matches_iso8601_pattern("2023"));
388 }
389
390 #[test]
391 fn test_unix_timestamp_bounds() {
392 assert!(!ScalarValue::is_valid_timestamp("0"));
394 assert!(!ScalarValue::is_valid_timestamp("1"));
395 assert!(!ScalarValue::is_valid_timestamp("4102444799"));
396 assert!(!ScalarValue::is_valid_timestamp("4102444800"));
397 assert!(ScalarValue::is_valid_timestamp("2001-12-14"));
398
399 assert!(ScalarValue::string("1")
401 .coerce_to_type(ScalarType::Timestamp)
402 .is_none());
403 assert_eq!(
404 ScalarValue::from(1_700_000_000i64).coerce_to_type(ScalarType::Timestamp),
405 None
406 );
407 assert_eq!(
408 ScalarValue::string("2001-12-14").coerce_to_type(ScalarType::Timestamp),
409 Some(ScalarValue::timestamp("2001-12-14"))
410 );
411 }
412
413 #[test]
414 fn test_iso8601_time_component_bounds() {
415 assert!(ScalarValue::matches_iso8601_pattern("2023-01-01T23:59:59"));
417 assert!(!ScalarValue::matches_iso8601_pattern("2023-01-01T24:00:00"));
418 assert!(!ScalarValue::matches_iso8601_pattern("2023-01-01T10:60:00"));
419 assert!(!ScalarValue::matches_iso8601_pattern("2023-01-01T10:30:60"));
420
421 assert!(ScalarValue::matches_iso8601_pattern("2023-01-31"));
423 assert!(!ScalarValue::matches_iso8601_pattern("2023-01-00"));
424 assert!(!ScalarValue::matches_iso8601_pattern("2023-01-32"));
425
426 assert!(!ScalarValue::matches_iso8601_pattern("2023-01-01X10:30:45"));
428 assert!(!ScalarValue::matches_iso8601_pattern("2023-01-01T10:30:xx"));
430
431 assert!(!ScalarValue::matches_iso8601_pattern("2023X12-25"));
435 assert!(!ScalarValue::matches_iso8601_pattern("2023-12X25"));
436 }
437
438 #[test]
439 fn test_timestamp_rejects_impossible_dates_and_trailing_text() {
440 for text in [
441 "2023-02-30",
442 "2023-02-29",
443 "2023-04-31",
444 "2100-02-29",
445 "2023-01-01T10:00:00garbage",
446 "2023-01-01T10:00:00Zx",
447 "2023-01-01T10:00:00+",
448 "2023-01-01T10:00:00+01:0",
449 ] {
450 assert!(!ScalarValue::matches_iso8601_pattern(text), "{text}");
451 assert_eq!(
452 ScalarValue::auto_detect_type(text),
453 ScalarType::String,
454 "{text}"
455 );
456 }
457 for text in [
458 "2024-02-29",
459 "2000-02-29",
460 "2023-01-01T10:00:00Z",
461 "2023-01-01T10:00:00.123456Z",
462 "2023-01-01T10:00:00.5+01:00",
463 "2001-12-14 21:59:43.10 -5",
464 "2001-12-14t21:59:43.10-05:00",
465 ] {
466 assert!(ScalarValue::matches_iso8601_pattern(text), "{text}");
467 }
468 }
469}