use crate::error::{Error, Result};
use crate::runtime::{Runtime, Value};
use chrono::{Local, LocalResult, NaiveDate, TimeZone, Utc};
use regex::Regex;
use std::cmp::Ordering;
pub(crate) fn is_literal_pattern(pat: &str) -> bool {
!pat.bytes().any(|b| {
matches!(
b,
b'.' | b'*'
| b'+'
| b'?'
| b'['
| b']'
| b'('
| b')'
| b'{'
| b'}'
| b'|'
| b'^'
| b'$'
| b'\\'
)
})
}
#[inline]
pub(crate) fn gsub_literal_eligible(re_pat: &str, repl: &str) -> bool {
is_literal_pattern(re_pat) && !repl.contains('&') && !repl.contains('\\')
}
fn literal_substring_absent(rt: &mut Runtime, needle: &str, hay: &str) -> bool {
if needle.is_empty() {
return !hay.contains(needle);
}
rt.literal_substring_finder(needle)
.find(hay.as_bytes())
.is_none()
}
fn literal_replace_all(s: &str, needle: &str, repl: &str, rt: &mut Runtime) -> (String, usize) {
if needle.is_empty() {
return (s.to_string(), 0);
}
let finder = rt.literal_substring_finder(needle);
let hay = s.as_bytes();
let mut out = String::with_capacity(s.len());
let mut count = 0usize;
let mut last = 0usize;
let mut off = 0usize;
while off < hay.len() {
let Some(rel) = finder.find(&hay[off..]) else {
break;
};
let abs = off + rel;
out.push_str(&s[last..abs]);
out.push_str(repl);
count += 1;
last = abs + needle.len();
off = last;
}
out.push_str(&s[last..]);
(out, count)
}
pub fn gsub(
rt: &mut Runtime,
re_pat: &str,
repl: &str,
target: Option<&mut String>,
) -> Result<f64> {
let repl_has_special = repl.contains('&') || repl.contains('\\');
let use_literal = is_literal_pattern(re_pat) && !repl_has_special;
let n = if let Some(t) = target {
if use_literal {
if literal_substring_absent(rt, re_pat, t) {
0
} else {
let (new_s, c) = literal_replace_all(t.as_str(), re_pat, repl, rt);
*t = new_s;
c
}
} else {
rt.ensure_regex(re_pat).map_err(Error::Runtime)?;
let re = rt.regex_ref(re_pat);
if !re.is_match(t.as_str()) {
0
} else {
let (new_s, c) = replace_all_awk(re, t.as_str(), repl, repl_has_special);
*t = new_s;
c
}
}
} else {
let cur = std::mem::take(&mut rt.record);
let (new_s, c) = if use_literal {
if literal_substring_absent(rt, re_pat, &cur) {
rt.record = cur;
return Ok(0.0);
}
literal_replace_all(&cur, re_pat, repl, rt)
} else {
rt.ensure_regex(re_pat).map_err(Error::Runtime)?;
let re = rt.regex_ref(re_pat);
if !re.is_match(&cur) {
rt.record = cur;
return Ok(0.0);
}
replace_all_awk(re, &cur, repl, repl_has_special)
};
drop(cur);
let fs = rt
.vars
.get("FS")
.map(|v| v.as_str())
.unwrap_or_else(|| " ".into());
rt.set_field_sep_split_owned(&fs, new_s);
c
};
Ok(n as f64)
}
pub fn sub_fn(
rt: &mut Runtime,
re_pat: &str,
repl: &str,
target: Option<&mut String>,
) -> Result<f64> {
rt.ensure_regex(re_pat).map_err(Error::Runtime)?;
let repl_has_special = repl.contains('&') || repl.contains('\\');
let n = if let Some(t) = target {
if let Some(m) = rt.regex_ref(re_pat).find(t.as_str()) {
let piece = if repl_has_special {
expand_repl(repl, m.as_str())
} else {
repl.to_string()
};
let mut out = String::with_capacity(t.len() + piece.len());
out.push_str(&t[..m.start()]);
out.push_str(&piece);
out.push_str(&t[m.end()..]);
*t = out;
1.0
} else {
0.0
}
} else {
let cur = std::mem::take(&mut rt.record);
if let Some(m) = rt.regex_ref(re_pat).find(&cur) {
let piece = if repl_has_special {
expand_repl(repl, m.as_str())
} else {
repl.to_string()
};
let mut out = String::with_capacity(cur.len() + piece.len());
out.push_str(&cur[..m.start()]);
out.push_str(&piece);
out.push_str(&cur[m.end()..]);
drop(cur);
let fs = rt
.vars
.get("FS")
.map(|v| v.as_str())
.unwrap_or_else(|| " ".into());
rt.set_field_sep_split_owned(&fs, out);
1.0
} else {
rt.record = cur;
0.0
}
};
Ok(n)
}
pub fn match_fn(rt: &mut Runtime, s: &str, re_pat: &str, arr_name: Option<&str>) -> Result<f64> {
rt.ensure_regex(re_pat).map_err(Error::Runtime)?;
let re = rt.regex_ref(re_pat).clone();
if let Some(m) = re.find(s) {
let rstart = (m.start() + 1) as f64;
let rlength = m.len() as f64;
rt.vars.insert("RSTART".into(), Value::Num(rstart));
rt.vars.insert("RLENGTH".into(), Value::Num(rlength));
if let Some(a) = arr_name {
rt.array_delete(a, None);
if let Some(caps) = re.captures(s) {
for i in 1..caps.len() {
let key = format!("{i}");
let val = caps
.get(i)
.map(|x| x.as_str().to_string())
.unwrap_or_default();
rt.array_set(a, key, Value::Str(val));
}
}
}
Ok(rstart)
} else {
rt.vars.insert("RSTART".into(), Value::Num(0.0));
rt.vars.insert("RLENGTH".into(), Value::Num(-1.0));
if let Some(a) = arr_name {
rt.array_delete(a, None);
}
Ok(0.0)
}
}
fn replace_all_awk(re: &Regex, s: &str, repl: &str, repl_has_special: bool) -> (String, usize) {
let mut count = 0usize;
let mut out = String::with_capacity(s.len());
let mut last = 0;
for m in re.find_iter(s) {
count += 1;
out.push_str(&s[last..m.start()]);
if repl_has_special {
out.push_str(&expand_repl(repl, m.as_str()));
} else {
out.push_str(repl);
}
last = m.end();
}
out.push_str(&s[last..]);
(out, count)
}
fn replace_nth_awk(re: &Regex, s: &str, repl: &str, repl_has_special: bool, n: usize) -> String {
if n == 0 {
let (out, _) = replace_all_awk(re, s, repl, repl_has_special);
return out;
}
let mut i = 0usize;
for m in re.find_iter(s) {
i += 1;
if i == n {
let piece = if repl_has_special {
expand_repl(repl, m.as_str())
} else {
repl.to_string()
};
let mut out = String::with_capacity(s.len());
out.push_str(&s[..m.start()]);
out.push_str(&piece);
out.push_str(&s[m.end()..]);
return out;
}
}
s.to_string()
}
pub fn awk_gensub(
rt: &mut Runtime,
ere: &str,
repl: &str,
how: &Value,
target: Option<String>,
) -> Result<String> {
let s = match target {
Some(t) => t,
None => rt.record.clone(),
};
rt.ensure_regex(ere).map_err(Error::Runtime)?;
let re = rt.regex_ref(ere).clone();
let s_ref = s.as_str();
let repl_has_special = repl.contains('&') || repl.contains('\\');
match how {
Value::Str(h) | Value::StrLit(h) => {
let h = h.trim();
if h.is_empty() {
return Err(Error::Runtime(
"gensub: third argument cannot be empty".into(),
));
}
if h.starts_with('g') || h.starts_with('G') {
let (out, _) = replace_all_awk(&re, s_ref, repl, repl_has_special);
Ok(out)
} else {
Err(Error::Runtime(format!(
"gensub: string third argument must begin with `g` or `G`, got `{h}`"
)))
}
}
Value::Num(n) => {
let which = *n as i64;
if which < 0 {
return Err(Error::Runtime(
"gensub: numeric third argument must be >= 0".into(),
));
}
Ok(replace_nth_awk(
&re,
s_ref,
repl,
repl_has_special,
which as usize,
))
}
_ => Err(Error::Runtime(
"gensub: third argument must be string or number".into(),
)),
}
}
fn expand_repl(repl: &str, matched: &str) -> String {
let mut out = String::new();
let mut chars = repl.chars().peekable();
while let Some(c) = chars.next() {
if c == '&' {
out.push_str(matched);
} else if c == '\\' {
match chars.peek() {
Some('&') => {
chars.next();
out.push('&');
}
Some('\\') => {
chars.next();
out.push('\\');
}
Some(x) => {
let x = *x;
chars.next();
out.push(x);
}
None => out.push('\\'),
}
} else {
out.push(c);
}
}
out
}
pub fn patsplit(
rt: &mut Runtime,
s: &str,
arr_name: &str,
fieldpat: Option<&str>,
seps_name: Option<&str>,
) -> Result<f64> {
let fp_owned = match fieldpat {
Some(s) => s.to_string(),
None => rt
.get_global_var("FPAT")
.map(|v| v.as_str())
.unwrap_or_default(),
};
let fp = if fp_owned.is_empty() {
"[^[:space:]]+"
} else {
fp_owned.as_str()
};
let re = Regex::new(fp).map_err(|e| Error::Runtime(e.to_string()))?;
let matches: Vec<regex::Match> = re.find_iter(s).collect();
let n = matches.len();
rt.array_delete(arr_name, None);
for (i, m) in matches.iter().enumerate() {
rt.array_set(
arr_name,
format!("{}", i + 1),
Value::Str(m.as_str().to_string()),
);
}
if let Some(sep_arr) = seps_name {
rt.array_delete(sep_arr, None);
for i in 1..n {
let prev = &matches[i - 1];
let curr = &matches[i];
let sep = &s[prev.end()..curr.start()];
rt.array_set(sep_arr, format!("{i}"), Value::Str(sep.to_string()));
}
}
Ok(n as f64)
}
pub fn awk_systime() -> f64 {
use std::time::{SystemTime, UNIX_EPOCH};
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs_f64())
.unwrap_or(0.0)
}
pub fn awk_strftime(args: &[Value]) -> std::result::Result<Value, String> {
let (fmt, ts, utc) = match args.len() {
0 => ("%c".to_string(), awk_systime(), false),
1 => (args[0].as_str(), awk_systime(), false),
2 => (args[0].as_str(), args[1].as_number(), false),
3 => (
args[0].as_str(),
args[1].as_number(),
args[2].as_number() != 0.0,
),
_ => return Err("strftime: expected 0 to 3 arguments".into()),
};
let secs = ts.floor() as i64;
let nsec = ((ts - secs as f64) * 1e9).round().clamp(0.0, 1e9 - 1.0) as u32;
let out = if utc {
Utc.timestamp_opt(secs, nsec)
.single()
.ok_or_else(|| "strftime: timestamp out of range".to_string())?
.format(&fmt)
.to_string()
} else {
Local
.timestamp_opt(secs, nsec)
.single()
.ok_or_else(|| "strftime: timestamp out of range".to_string())?
.format(&fmt)
.to_string()
};
Ok(Value::Str(out))
}
pub fn awk_mktime(s: &str) -> f64 {
let parts: Vec<&str> = s.split_whitespace().collect();
if parts.len() < 6 {
return -1.0;
}
let y: i32 = match parts[0].parse() {
Ok(v) => v,
Err(_) => return -1.0,
};
let mo: u32 = match parts[1].parse() {
Ok(v) => v,
Err(_) => return -1.0,
};
let d: u32 = match parts[2].parse() {
Ok(v) => v,
Err(_) => return -1.0,
};
let h: u32 = match parts[3].parse() {
Ok(v) => v,
Err(_) => return -1.0,
};
let mi: u32 = match parts[4].parse() {
Ok(v) => v,
Err(_) => return -1.0,
};
let se: u32 = match parts[5].parse() {
Ok(v) => v,
Err(_) => return -1.0,
};
let naive = match NaiveDate::from_ymd_opt(y, mo, d) {
Some(date) => match date.and_hms_opt(h, mi, se) {
Some(n) => n,
None => return -1.0,
},
None => return -1.0,
};
match Local.from_local_datetime(&naive) {
LocalResult::Single(dt) => dt.timestamp() as f64,
LocalResult::Ambiguous(_, _) | LocalResult::None => -1.0,
}
}
#[inline]
fn num_to_u64(n: f64) -> u64 {
n.trunc() as i64 as u64
}
pub fn awk_and(a: f64, b: f64) -> f64 {
(num_to_u64(a) & num_to_u64(b)) as i64 as f64
}
pub fn awk_or(a: f64, b: f64) -> f64 {
(num_to_u64(a) | num_to_u64(b)) as i64 as f64
}
pub fn awk_xor(a: f64, b: f64) -> f64 {
(num_to_u64(a) ^ num_to_u64(b)) as i64 as f64
}
pub fn awk_lshift(a: f64, b: f64) -> f64 {
let x = num_to_u64(a);
let n = (num_to_u64(b) & 0x3f) as u32;
(x << n) as i64 as f64
}
pub fn awk_rshift(a: f64, b: f64) -> f64 {
let x = num_to_u64(a);
let n = (num_to_u64(b) & 0x3f) as u32;
(x >> n) as i64 as f64
}
pub fn awk_compl(a: f64) -> f64 {
(!num_to_u64(a)) as i64 as f64
}
pub fn awk_strtonum(s: &str) -> f64 {
let t = s.trim();
if t.is_empty() {
return 0.0;
}
if t.starts_with("0x") || t.starts_with("0X") {
return u64::from_str_radix(&t[2..], 16)
.map(|v| v as f64)
.unwrap_or(0.0);
}
if t.len() > 1 && t.starts_with('0') && !t.contains('.') && !t.contains('e') && !t.contains('E')
{
return i64::from_str_radix(t, 8).map(|v| v as f64).unwrap_or(0.0);
}
t.parse::<f64>().unwrap_or(0.0)
}
fn locale_str_cmp_sort(a: &str, b: &str) -> Ordering {
#[cfg(unix)]
{
use std::ffi::CString;
match (CString::new(a), CString::new(b)) {
(Ok(ca), Ok(cb)) => unsafe {
let r = libc::strcoll(ca.as_ptr(), cb.as_ptr());
r.cmp(&0)
},
_ => a.cmp(b),
}
}
#[cfg(not(unix))]
{
a.cmp(b)
}
}
pub fn awk_value_sort_cmp(a: &Value, b: &Value) -> Ordering {
if let (Value::Num(x), Value::Num(y)) = (a, b) {
return x.partial_cmp(y).unwrap_or(Ordering::Equal);
}
if a.is_numeric_str() && b.is_numeric_str() {
return a
.as_number()
.partial_cmp(&b.as_number())
.unwrap_or(Ordering::Equal);
}
locale_str_cmp_sort(&a.as_str(), &b.as_str())
}
pub fn asort(rt: &mut Runtime, src: &str, dest: Option<&str>) -> Result<f64> {
let mut pairs: Vec<(String, Value)> = match rt.get_global_var(src) {
Some(Value::Array(a)) => a.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
_ => return Err(Error::Runtime(format!("asort: `{src}` is not an array"))),
};
pairs.sort_by(|(_, va), (_, vb)| awk_value_sort_cmp(va, vb));
let n = pairs.len();
match dest {
None => {
rt.array_delete(src, None);
for (i, (_, v)) in pairs.iter().enumerate() {
rt.array_set(src, format!("{}", i + 1), v.clone());
}
}
Some(d) if d == src => {
rt.array_delete(src, None);
for (i, (_, v)) in pairs.iter().enumerate() {
rt.array_set(src, format!("{}", i + 1), v.clone());
}
}
Some(d) => {
rt.array_delete(d, None);
for (i, (_, v)) in pairs.iter().enumerate() {
rt.array_set(d, format!("{}", i + 1), v.clone());
}
}
}
Ok(n as f64)
}
pub fn asorti(rt: &mut Runtime, src: &str, dest: Option<&str>) -> Result<f64> {
let mut keys: Vec<String> = match rt.get_global_var(src) {
Some(Value::Array(a)) => a.keys().cloned().collect(),
_ => return Err(Error::Runtime(format!("asorti: `{src}` is not an array"))),
};
keys.sort_by(|a, b| locale_str_cmp_sort(a, b));
let n = keys.len();
match dest {
None => {
rt.array_delete(src, None);
for (i, k) in keys.iter().enumerate() {
rt.array_set(src, format!("{}", i + 1), Value::Str(k.clone()));
}
}
Some(d) if d == src => {
rt.array_delete(src, None);
for (i, k) in keys.iter().enumerate() {
rt.array_set(src, format!("{}", i + 1), Value::Str(k.clone()));
}
}
Some(d) => {
rt.array_delete(d, None);
for (i, k) in keys.iter().enumerate() {
rt.array_set(d, format!("{}", i + 1), Value::Str(k.clone()));
}
}
}
Ok(n as f64)
}
#[inline]
pub fn awk_typeof_value(v: &Value) -> &'static str {
match v {
Value::Uninit => "uninitialized",
Value::Num(_) => "number",
Value::Mpfr(_) => "number",
Value::Str(_) | Value::StrLit(_) => "string",
Value::Regexp(_) => "regexp",
Value::Array(_) => "array",
}
}
pub fn awk_typeof_array_elem(rt: &Runtime, name: &str, key: &str) -> &'static str {
match rt.get_global_var(name) {
Some(Value::Array(a)) => a
.get(key)
.map(|v| awk_typeof_value(v))
.unwrap_or("uninitialized"),
_ => "uninitialized",
}
}
#[cfg(test)]
mod tests {
use super::{
asort, asorti, awk_gensub, awk_mktime, awk_strftime, awk_strtonum, awk_typeof_array_elem,
awk_typeof_value, awk_value_sort_cmp, gsub, gsub_literal_eligible, is_literal_pattern,
match_fn, patsplit, sub_fn,
};
use crate::runtime::{Runtime, Value};
use std::cmp::Ordering;
fn rt_with_fs() -> Runtime {
let mut rt = Runtime::new();
rt.vars.insert("FS".into(), Value::Str(" ".into()));
rt
}
#[test]
fn gsub_literal_on_record_replaces_and_resplits() {
let mut rt = rt_with_fs();
rt.record = "foofoo".into();
let n = gsub(&mut rt, "foo", "bar", None).unwrap();
assert_eq!(n, 2.0);
assert_eq!(rt.record, "barbar");
}
#[test]
fn gsub_regex_with_amp_replacement() {
let mut rt = rt_with_fs();
rt.record = "ab".into();
let n = gsub(&mut rt, "a", "X&Y", None).unwrap();
assert_eq!(n, 1.0);
assert_eq!(rt.record, "XaYb");
}
#[test]
fn sub_first_match_only() {
let mut rt = rt_with_fs();
rt.record = "aaa".into();
let n = sub_fn(&mut rt, "a", "b", None).unwrap();
assert_eq!(n, 1.0);
assert_eq!(rt.record, "baa");
}
#[test]
fn match_sets_rstart_rlength_on_hit() {
let mut rt = Runtime::new();
let n = match_fn(&mut rt, "foo123bar", "[0-9]+", None).unwrap();
assert_eq!(n, 4.0);
assert_eq!(rt.vars.get("RSTART").unwrap().as_number(), 4.0);
assert_eq!(rt.vars.get("RLENGTH").unwrap().as_number(), 3.0);
}
#[test]
fn match_sets_rstart_zero_on_miss() {
let mut rt = Runtime::new();
let n = match_fn(&mut rt, "abc", "[0-9]+", None).unwrap();
assert_eq!(n, 0.0);
assert_eq!(rt.vars.get("RSTART").unwrap().as_number(), 0.0);
assert_eq!(rt.vars.get("RLENGTH").unwrap().as_number(), -1.0);
}
#[test]
fn match_with_array_fills_capture_groups() {
let mut rt = Runtime::new();
let n = match_fn(&mut rt, "foo123bar", "([a-z]+)([0-9]+)", Some("cap")).unwrap();
assert_eq!(n, 1.0);
assert_eq!(rt.array_get("cap", "1").as_str(), "foo");
assert_eq!(rt.array_get("cap", "2").as_str(), "123");
}
#[test]
fn match_miss_clears_named_array() {
let mut rt = Runtime::new();
rt.array_set("cap", "1".into(), Value::Str("keep".into()));
let n = match_fn(&mut rt, "zzz", "a+", Some("cap")).unwrap();
assert_eq!(n, 0.0);
assert_eq!(awk_typeof_array_elem(&rt, "cap", "1"), "uninitialized");
}
#[test]
fn patsplit_fills_array() {
let mut rt = Runtime::new();
let n = patsplit(&mut rt, "x y z", "parts", Some("[a-z]+"), None).unwrap();
assert_eq!(n, 3.0);
assert_eq!(rt.array_get("parts", "1").as_str(), "x");
assert_eq!(rt.array_get("parts", "2").as_str(), "y");
assert_eq!(rt.array_get("parts", "3").as_str(), "z");
}
#[test]
fn is_literal_pattern_accepts_plain_text() {
assert!(is_literal_pattern("hello"));
}
#[test]
fn is_literal_pattern_rejects_regex_metachar() {
assert!(!is_literal_pattern("a.c"));
}
#[test]
fn gsub_literal_eligible_rejects_ampersand_in_replacement() {
assert!(!gsub_literal_eligible("x", "a&b"));
}
#[test]
fn gsub_literal_eligible_accepts_simple_pair() {
assert!(gsub_literal_eligible("needle", "repl"));
}
#[test]
fn awk_typeof_value_variants() {
assert_eq!(awk_typeof_value(&Value::Uninit), "uninitialized");
assert_eq!(awk_typeof_value(&Value::Num(0.0)), "number");
assert_eq!(awk_typeof_value(&Value::Str("".into())), "string");
assert_eq!(awk_typeof_value(&Value::StrLit("x".into())), "string");
assert_eq!(awk_typeof_value(&Value::Regexp(".".into())), "regexp");
let mut a = crate::runtime::AwkMap::default();
a.insert("k".into(), Value::Num(1.0));
assert_eq!(awk_typeof_value(&Value::Array(a)), "array");
}
#[test]
fn awk_typeof_array_elem_hits_and_misses() {
let mut rt = Runtime::new();
rt.array_set("t", "k".into(), Value::Str("v".into()));
assert_eq!(awk_typeof_array_elem(&rt, "t", "k"), "string");
assert_eq!(awk_typeof_array_elem(&rt, "t", "missing"), "uninitialized");
assert_eq!(
awk_typeof_array_elem(&rt, "not_array", "k"),
"uninitialized"
);
}
#[test]
fn awk_strtonum_empty_or_whitespace_zero() {
assert_eq!(awk_strtonum(""), 0.0);
assert_eq!(awk_strtonum(" "), 0.0);
}
#[test]
fn awk_strtonum_hex_octal_and_decimal() {
assert_eq!(awk_strtonum("0x10"), 16.0);
assert_eq!(awk_strtonum("0Xff"), 255.0);
assert_eq!(awk_strtonum("010"), 8.0);
assert_eq!(awk_strtonum("3.5"), 3.5);
}
#[test]
fn awk_strtonum_invalid_hex_returns_zero() {
assert_eq!(awk_strtonum("0x"), 0.0);
assert_eq!(awk_strtonum("0xzz"), 0.0);
}
#[test]
fn awk_strtonum_hex_above_u64_returns_zero() {
assert_eq!(awk_strtonum("0x10000000000000000"), 0.0);
}
#[test]
fn awk_value_sort_cmp_numeric_strings_use_numeric_order() {
assert_eq!(
awk_value_sort_cmp(&Value::Str("2".into()), &Value::Str("10".into())),
Ordering::Less
);
}
#[test]
fn awk_value_sort_cmp_plain_numbers() {
assert_eq!(
awk_value_sort_cmp(&Value::Num(3.0), &Value::Num(1.0)),
Ordering::Greater
);
}
#[test]
fn asort_inplace_sorts_values_and_reindexes_one_based() {
let mut rt = Runtime::new();
rt.array_set("a", "z".into(), Value::Num(30.0));
rt.array_set("a", "y".into(), Value::Num(10.0));
rt.array_set("a", "x".into(), Value::Num(20.0));
let n = asort(&mut rt, "a", None).unwrap();
assert_eq!(n, 3.0);
assert_eq!(rt.array_get("a", "1").as_number(), 10.0);
assert_eq!(rt.array_get("a", "2").as_number(), 20.0);
assert_eq!(rt.array_get("a", "3").as_number(), 30.0);
}
#[test]
fn asort_into_destination_leaves_source_unchanged() {
let mut rt = Runtime::new();
rt.array_set("src", "p".into(), Value::Num(2.0));
rt.array_set("src", "q".into(), Value::Num(1.0));
let n = asort(&mut rt, "src", Some("dst")).unwrap();
assert_eq!(n, 2.0);
assert_eq!(rt.array_get("dst", "1").as_number(), 1.0);
assert_eq!(rt.array_get("dst", "2").as_number(), 2.0);
assert_eq!(rt.array_get("src", "p").as_number(), 2.0);
assert_eq!(rt.array_get("src", "q").as_number(), 1.0);
}
#[test]
fn asort_non_array_errors() {
let mut rt = Runtime::new();
rt.vars.insert("x".into(), Value::Num(1.0));
let e = asort(&mut rt, "x", None).unwrap_err();
assert!(e.to_string().contains("asort"), "{e}");
}
#[test]
fn asorti_inplace_sorts_keys_into_string_values() {
let mut rt = Runtime::new();
rt.array_set("a", "banana".into(), Value::Num(1.0));
rt.array_set("a", "apple".into(), Value::Num(2.0));
let n = asorti(&mut rt, "a", None).unwrap();
assert_eq!(n, 2.0);
assert_eq!(rt.array_get("a", "1").as_str(), "apple");
assert_eq!(rt.array_get("a", "2").as_str(), "banana");
}
#[test]
fn asorti_non_array_errors() {
let mut rt = Runtime::new();
let e = asorti(&mut rt, "missing", None).unwrap_err();
assert!(e.to_string().contains("asorti"), "{e}");
}
#[test]
fn awk_mktime_short_datespec_returns_minus_one() {
assert_eq!(awk_mktime("2020 1"), -1.0);
assert_eq!(awk_mktime("not numbers"), -1.0);
}
#[test]
fn awk_mktime_six_field_datespec_positive() {
let t = awk_mktime("2020 06 15 12 00 00");
assert!(t > 0.0, "expected positive epoch seconds, got {t}");
}
#[test]
fn awk_strftime_no_args_yields_non_empty_string() {
let v = awk_strftime(&[]).unwrap();
assert!(!v.as_str().is_empty(), "{v:?}");
}
#[test]
fn awk_gensub_global_string_replaces_all_matches() {
let mut rt = Runtime::new();
rt.record = "a1b2c".into();
let out = awk_gensub(&mut rt, "[0-9]", "X", &Value::Str("g".into()), None).unwrap();
assert_eq!(out, "aXbXc");
}
#[test]
fn awk_gensub_numeric_zero_replaces_all_like_g() {
let mut rt = Runtime::new();
let out = awk_gensub(
&mut rt,
"[0-9]",
"_",
&Value::Num(0.0),
Some("z9y9z".into()),
)
.unwrap();
assert_eq!(out, "z_y_z");
}
#[test]
fn awk_gensub_numeric_two_replaces_second_match_only() {
let mut rt = Runtime::new();
let out = awk_gensub(
&mut rt,
"[0-9]",
"X",
&Value::Num(2.0),
Some("a1b2c3".into()),
)
.unwrap();
assert_eq!(out, "a1bXc3");
}
#[test]
fn awk_gensub_empty_how_string_errors() {
let mut rt = Runtime::new();
let e = awk_gensub(
&mut rt,
"a",
"b",
&Value::Str(" ".into()),
Some("x".into()),
)
.unwrap_err();
assert!(e.to_string().contains("gensub"), "{e}");
}
#[test]
fn awk_gensub_negative_numeric_how_errors() {
let mut rt = Runtime::new();
let e = awk_gensub(&mut rt, "a", "b", &Value::Num(-1.0), Some("x".into())).unwrap_err();
assert!(e.to_string().contains("gensub"), "{e}");
}
}