Skip to main content

awk_rs/
value.rs

1use std::borrow::Cow;
2use std::cmp::Ordering;
3use std::fmt;
4
5/// AWK value type with dynamic typing and automatic coercion
6///
7/// AWK has a unique type system where values can be strings, numbers, or
8/// "numeric strings" (strings that look like numbers). This enum captures
9/// all three cases.
10///
11/// # Examples
12///
13/// ```
14/// use awk_rs::Value;
15///
16/// // Numbers
17/// let num = Value::Number(42.0);
18/// assert_eq!(num.to_number(), 42.0);
19/// assert_eq!(num.to_string_val(), "42");
20///
21/// // Strings
22/// let s = Value::from_string("hello".to_string());
23/// assert_eq!(s.to_string_val(), "hello");
24/// assert_eq!(s.to_number(), 0.0);  // Non-numeric string coerces to 0
25///
26/// // Numeric strings
27/// let ns = Value::from_string("123".to_string());
28/// assert_eq!(ns.to_number(), 123.0);
29/// assert_eq!(ns.to_string_val(), "123");
30///
31/// // Truthiness
32/// assert!(Value::Number(1.0).is_truthy());
33/// assert!(!Value::Number(0.0).is_truthy());
34/// assert!(Value::from_string("hello".to_string()).is_truthy());
35/// assert!(!Value::from_string("".to_string()).is_truthy());
36/// ```
37#[derive(Debug, Clone, Default)]
38pub enum Value {
39    /// Uninitialized value - coerces to "" or 0 depending on context
40    #[default]
41    Uninitialized,
42    /// Numeric value
43    Number(f64),
44    /// String value
45    String(String),
46    /// Numeric string - a string that looks like a number
47    /// (used for comparison semantics)
48    NumericString(String, f64),
49}
50
51impl Value {
52    /// Create a new string value, detecting if it's a numeric string
53    #[inline]
54    pub fn from_string(s: String) -> Self {
55        if let Some(num) = parse_numeric_string(&s) {
56            Value::NumericString(s, num)
57        } else {
58            Value::String(s)
59        }
60    }
61
62    /// Create a numeric value
63    #[inline]
64    pub fn from_number(n: f64) -> Self {
65        Value::Number(n)
66    }
67
68    /// Check if this value is "true" in boolean context
69    /// - Uninitialized is false
70    /// - Number 0 is false
71    /// - Empty string is false
72    /// - Everything else is true
73    #[inline]
74    pub fn is_truthy(&self) -> bool {
75        match self {
76            Value::Uninitialized => false,
77            Value::Number(n) => *n != 0.0,
78            Value::String(s) => !s.is_empty(),
79            Value::NumericString(s, _) => !s.is_empty(),
80        }
81    }
82
83    /// Coerce to numeric value
84    #[inline]
85    pub fn to_number(&self) -> f64 {
86        match self {
87            Value::Uninitialized => 0.0,
88            Value::Number(n) => *n,
89            Value::String(s) => parse_leading_number(s),
90            Value::NumericString(_, n) => *n,
91        }
92    }
93
94    /// Coerce to string value
95    #[inline]
96    pub fn to_string_val(&self) -> String {
97        self.to_string_with_format("%.6g")
98    }
99
100    /// Get string as Cow to avoid allocation when possible
101    #[inline]
102    pub fn as_str(&self) -> Cow<'_, str> {
103        match self {
104            Value::Uninitialized => Cow::Borrowed(""),
105            Value::Number(n) => Cow::Owned(format_number(*n, "%.6g")),
106            Value::String(s) => Cow::Borrowed(s),
107            Value::NumericString(s, _) => Cow::Borrowed(s),
108        }
109    }
110
111    /// Coerce to string with specific format (for OFMT/CONVFMT)
112    pub fn to_string_with_format(&self, format: &str) -> String {
113        match self {
114            Value::Uninitialized => String::new(),
115            Value::Number(n) => format_number(*n, format),
116            Value::String(s) => s.clone(),
117            Value::NumericString(s, _) => s.clone(),
118        }
119    }
120
121    /// Check if this value is definitely numeric
122    #[inline]
123    pub fn is_numeric(&self) -> bool {
124        matches!(self, Value::Number(_))
125    }
126
127    /// Check if this value is a numeric string
128    #[inline]
129    pub fn is_numeric_string(&self) -> bool {
130        matches!(self, Value::NumericString(_, _))
131    }
132
133    /// Check if this value should compare as a number
134    #[inline]
135    pub fn compares_as_number(&self) -> bool {
136        matches!(
137            self,
138            Value::Number(_) | Value::NumericString(_, _) | Value::Uninitialized
139        )
140    }
141}
142
143impl fmt::Display for Value {
144    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
145        match self {
146            Value::Uninitialized => Ok(()),
147            Value::Number(n) => write!(f, "{}", format_number(*n, "%.6g")),
148            Value::String(s) => write!(f, "{}", s),
149            Value::NumericString(s, _) => write!(f, "{}", s),
150        }
151    }
152}
153
154/// Compare two AWK values according to AWK comparison rules
155#[inline]
156pub fn compare_values(left: &Value, right: &Value) -> Ordering {
157    compare_values_with_format(left, right, "%.6g")
158}
159
160/// Compare two AWK values, using `convfmt` (CONVFMT) when a number has to be
161/// converted to a string for a string comparison.
162#[inline]
163pub fn compare_values_with_format(left: &Value, right: &Value, convfmt: &str) -> Ordering {
164    // If both are numeric or numeric strings, compare numerically
165    if left.compares_as_number() && right.compares_as_number() {
166        let l = left.to_number();
167        let r = right.to_number();
168        l.partial_cmp(&r).unwrap_or(Ordering::Equal)
169    } else {
170        // Otherwise compare as strings
171        match (left, right) {
172            // Slow path: at least one Number operand needs CONVFMT formatting.
173            (Value::Number(_), _) | (_, Value::Number(_)) => left
174                .to_string_with_format(convfmt)
175                .cmp(&right.to_string_with_format(convfmt)),
176            // Fast path: no Number operand, so CONVFMT is never applied.
177            _ => left.as_str().cmp(&right.as_str()),
178        }
179    }
180}
181
182/// Parse the leading numeric portion of a string using optimized byte-based parsing
183/// "42abc" -> 42.0
184/// "  3.14  " -> 3.14
185/// "abc" -> 0.0
186#[inline]
187pub fn parse_leading_number(s: &str) -> f64 {
188    let bytes = s.as_bytes();
189    let mut i = 0;
190
191    // Skip leading whitespace
192    while i < bytes.len() && bytes[i].is_ascii_whitespace() {
193        i += 1;
194    }
195
196    if i >= bytes.len() {
197        return 0.0;
198    }
199
200    let start = i;
201
202    // Optional sign
203    if i < bytes.len() && (bytes[i] == b'+' || bytes[i] == b'-') {
204        i += 1;
205    }
206
207    let mut has_digits = false;
208
209    // Digits before decimal
210    while i < bytes.len() && bytes[i].is_ascii_digit() {
211        i += 1;
212        has_digits = true;
213    }
214
215    // Decimal point and digits after
216    if i < bytes.len() && bytes[i] == b'.' {
217        i += 1;
218        while i < bytes.len() && bytes[i].is_ascii_digit() {
219            i += 1;
220            has_digits = true;
221        }
222    }
223
224    // Exponent
225    if i < bytes.len() && (bytes[i] == b'e' || bytes[i] == b'E') {
226        let exp_start = i;
227        i += 1;
228        if i < bytes.len() && (bytes[i] == b'+' || bytes[i] == b'-') {
229            i += 1;
230        }
231        if i < bytes.len() && bytes[i].is_ascii_digit() {
232            while i < bytes.len() && bytes[i].is_ascii_digit() {
233                i += 1;
234            }
235        } else {
236            // Invalid exponent, back up
237            i = exp_start;
238        }
239    }
240
241    if !has_digits {
242        return 0.0;
243    }
244
245    // Fast path for common integer case
246    let num_str = &s[start..i];
247    let is_integer_like =
248        !num_str.contains('.') && !num_str.contains('e') && !num_str.contains('E');
249    match num_str.parse::<i64>() {
250        Ok(n) if is_integer_like => return n as f64,
251        _ => {}
252    }
253
254    num_str.parse().unwrap_or(0.0)
255}
256
257/// Check if a string is a numeric string (looks entirely like a number)
258#[inline]
259fn parse_numeric_string(s: &str) -> Option<f64> {
260    let trimmed = s.trim();
261    if trimmed.is_empty() {
262        return None;
263    }
264
265    // Fast path: check if it's a simple integer
266    if trimmed.bytes().all(|b| b.is_ascii_digit()) {
267        return trimmed.parse().ok();
268    }
269
270    // Check for leading sign
271    let check = if trimmed.starts_with('-') || trimmed.starts_with('+') {
272        &trimmed[1..]
273    } else {
274        trimmed
275    };
276
277    // Simple float pattern check
278    let mut has_dot = false;
279    let mut has_e = false;
280    for (i, b) in check.bytes().enumerate() {
281        match b {
282            b'0'..=b'9' => continue,
283            b'.' if !has_dot && !has_e => has_dot = true,
284            b'e' | b'E' if !has_e && i > 0 => {
285                has_e = true;
286                // Check for sign after e
287                if i + 1 < check.len() {
288                    let next = check.as_bytes()[i + 1];
289                    if next == b'+' || next == b'-' {
290                        continue;
291                    }
292                }
293            }
294            b'+' | b'-' if has_e => continue,
295            _ => return None,
296        }
297    }
298
299    trimmed.parse().ok()
300}
301
302/// Format a number according to printf-style format
303pub fn format_number(n: f64, format: &str) -> String {
304    if n.is_nan() {
305        return "nan".to_string();
306    }
307    if n.is_infinite() {
308        return if n > 0.0 { "inf" } else { "-inf" }.to_string();
309    }
310
311    // POSIX: values that are exactly integral convert as integers, with no
312    // format applied at all.
313    if n.fract() == 0.0 {
314        if n.abs() < 1e15 {
315            return itoa_fast(n as i64);
316        }
317        return format!("{:.0}", n);
318    }
319
320    crate::fmt::sprintf(format, &[Value::Number(n)])
321}
322
323/// Fast integer to string conversion
324#[inline]
325fn itoa_fast(n: i64) -> String {
326    if n == 0 {
327        return "0".to_string();
328    }
329
330    let mut result = String::with_capacity(20);
331    let mut num = n;
332    let negative = num < 0;
333    if negative {
334        num = -num;
335    }
336
337    while num > 0 {
338        result.push((b'0' + (num % 10) as u8) as char);
339        num /= 10;
340    }
341
342    if negative {
343        result.push('-');
344    }
345
346    result.chars().rev().collect()
347}
348
349#[cfg(test)]
350mod tests {
351    use super::*;
352
353    #[test]
354    fn test_uninitialized() {
355        let v = Value::Uninitialized;
356        assert_eq!(v.to_number(), 0.0);
357        assert_eq!(v.to_string_val(), "");
358        assert!(!v.is_truthy());
359    }
360
361    #[test]
362    fn test_number() {
363        let v = Value::Number(42.0);
364        assert_eq!(v.to_number(), 42.0);
365        assert_eq!(v.to_string_val(), "42");
366        assert!(v.is_truthy());
367
368        let zero = Value::Number(0.0);
369        assert!(!zero.is_truthy());
370    }
371
372    #[test]
373    fn test_string() {
374        let v = Value::from_string("hello".to_string());
375        assert_eq!(v.to_number(), 0.0);
376        assert_eq!(v.to_string_val(), "hello");
377        assert!(v.is_truthy());
378
379        let empty = Value::from_string("".to_string());
380        assert!(!empty.is_truthy());
381    }
382
383    #[test]
384    fn test_numeric_string() {
385        let v = Value::from_string("42".to_string());
386        assert!(v.is_numeric_string());
387        assert_eq!(v.to_number(), 42.0);
388        assert_eq!(v.to_string_val(), "42");
389    }
390
391    #[test]
392    fn test_leading_number() {
393        assert_eq!(parse_leading_number("42abc"), 42.0);
394        assert_eq!(parse_leading_number("  2.75  "), 2.75);
395        assert_eq!(parse_leading_number("abc"), 0.0);
396        assert_eq!(parse_leading_number("-5.5"), -5.5);
397        assert_eq!(parse_leading_number("1e10"), 1e10);
398    }
399
400    #[test]
401    fn test_comparison() {
402        let n1 = Value::Number(10.0);
403        let n2 = Value::Number(2.0);
404        assert_eq!(compare_values(&n1, &n2), Ordering::Greater);
405
406        let s1 = Value::from_string("10".to_string());
407        let s2 = Value::from_string("2".to_string());
408        // Both numeric strings -> compare numerically
409        assert_eq!(compare_values(&s1, &s2), Ordering::Greater);
410
411        let s3 = Value::from_string("abc".to_string());
412        let s4 = Value::from_string("def".to_string());
413        // Both pure strings -> compare lexically
414        assert_eq!(compare_values(&s3, &s4), Ordering::Less);
415    }
416
417    #[test]
418    fn test_itoa_fast() {
419        assert_eq!(itoa_fast(0), "0");
420        assert_eq!(itoa_fast(42), "42");
421        assert_eq!(itoa_fast(-123), "-123");
422        assert_eq!(itoa_fast(1000000), "1000000");
423    }
424
425    #[test]
426    fn test_format_number_nan() {
427        assert_eq!(format_number(f64::NAN, "%.6g"), "nan");
428    }
429
430    #[test]
431    fn test_format_number_inf() {
432        assert_eq!(format_number(f64::INFINITY, "%.6g"), "inf");
433        assert_eq!(format_number(f64::NEG_INFINITY, "%.6g"), "-inf");
434    }
435
436    #[test]
437    fn test_format_number_integer() {
438        assert_eq!(format_number(42.0, "%.6g"), "42");
439        assert_eq!(format_number(-100.0, "%.6g"), "-100");
440    }
441
442    #[test]
443    fn test_format_number_float() {
444        assert_eq!(format_number(2.75, "%.6g"), "2.75");
445    }
446
447    #[test]
448    fn test_from_number() {
449        let v = Value::from_number(2.75);
450        assert_eq!(v.to_number(), 2.75);
451    }
452
453    #[test]
454    fn test_is_truthy_numeric_string() {
455        let v = Value::NumericString("42".to_string(), 42.0);
456        assert!(v.is_truthy());
457
458        let empty = Value::NumericString("".to_string(), 0.0);
459        assert!(!empty.is_truthy());
460    }
461
462    #[test]
463    fn test_comparison_number_vs_string() {
464        let n = Value::Number(10.0);
465        let s = Value::from_string("hello".to_string());
466        // Number vs non-numeric string
467        assert!(compare_values(&n, &s) != Ordering::Equal);
468    }
469
470    #[test]
471    fn test_comparison_uninitialized() {
472        let u = Value::Uninitialized;
473        let n = Value::Number(1.0);
474        // Uninitialized (0) vs 1 should be Less
475        assert_eq!(compare_values(&u, &n), Ordering::Less);
476    }
477
478    #[test]
479    fn test_parse_leading_with_sign() {
480        assert_eq!(parse_leading_number("+42"), 42.0);
481        assert_eq!(parse_leading_number("  +2.75"), 2.75);
482    }
483
484    #[test]
485    fn test_parse_leading_exponent() {
486        assert_eq!(parse_leading_number("1e-5"), 1e-5);
487        assert_eq!(parse_leading_number("2E+3"), 2000.0);
488    }
489
490    #[test]
491    fn test_numeric_string_with_exponent() {
492        let v = Value::from_string("1e5".to_string());
493        assert!(v.is_numeric_string());
494        assert_eq!(v.to_number(), 1e5);
495    }
496
497    #[test]
498    fn test_numeric_string_with_sign() {
499        let v = Value::from_string("-42.5".to_string());
500        assert!(v.is_numeric_string());
501        assert_eq!(v.to_number(), -42.5);
502    }
503
504    #[test]
505    fn test_numeric_string_whitespace() {
506        let v = Value::from_string("  123  ".to_string());
507        assert!(v.is_numeric_string());
508        assert_eq!(v.to_number(), 123.0);
509    }
510
511    #[test]
512    fn test_to_string_val_uninitialized() {
513        let v = Value::Uninitialized;
514        assert_eq!(v.to_string_val(), "");
515    }
516
517    #[test]
518    fn test_to_number_uninitialized() {
519        let v = Value::Uninitialized;
520        assert_eq!(v.to_number(), 0.0);
521    }
522}