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.is_empty()
&& !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 && !rt.ignore_case_flag();
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) {
let subsep = rt
.get_global_var("SUBSEP")
.map(|v| v.as_str())
.unwrap_or_else(|| "\x1c".to_string());
for i in 0..caps.len() {
if let Some(g) = caps.get(i) {
let key = format!("{i}");
rt.array_set(a, key, Value::Str(g.as_str().to_string()));
let char_start = s[..g.start()].chars().count() + 1;
let char_len = g.as_str().chars().count();
rt.array_set(
a,
format!("{i}{subsep}start"),
Value::Num(char_start as f64),
);
rt.array_set(a, format!("{i}{subsep}length"), Value::Num(char_len as f64));
}
}
}
}
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_all_gensub(re: &Regex, s: &str, repl: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut last = 0;
for caps in re.captures_iter(s) {
let whole = caps.get(0).expect("group 0 always present");
out.push_str(&s[last..whole.start()]);
out.push_str(&expand_repl_with_caps(repl, &caps));
last = whole.end();
}
out.push_str(&s[last..]);
out
}
fn replace_nth_gensub(re: &Regex, s: &str, repl: &str, n: usize) -> String {
if n == 0 {
return replace_all_gensub(re, s, repl);
}
let mut i = 0usize;
for caps in re.captures_iter(s) {
i += 1;
if i == n {
let whole = caps.get(0).expect("group 0 always present");
let piece = expand_repl_with_caps(repl, &caps);
let mut out = String::with_capacity(s.len());
out.push_str(&s[..whole.start()]);
out.push_str(&piece);
out.push_str(&s[whole.end()..]);
return out;
}
}
s.to_string()
}
fn expand_repl_with_caps(repl: &str, caps: ®ex::Captures<'_>) -> String {
let matched = caps.get(0).map(|m| m.as_str()).unwrap_or("");
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(d) if d.is_ascii_digit() && *d != '0' => {
let n = d.to_digit(10).unwrap() as usize;
chars.next();
if let Some(g) = caps.get(n) {
out.push_str(g.as_str());
}
}
Some(x) => {
let x = *x;
chars.next();
out.push(x);
}
None => out.push('\\'),
}
} else {
out.push(c);
}
}
out
}
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();
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') {
Ok(replace_all_gensub(&re, s_ref, repl))
} 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).max(1) as usize;
Ok(replace_nth_gensub(&re, s_ref, repl, which))
}
_ => Err(Error::Runtime(
"gensub: third argument must be string or number".into(),
)),
}
}
fn expand_repl(repl: &str, matched: &str) -> String {
let bytes = repl.as_bytes();
let mut out = String::with_capacity(repl.len() + matched.len());
let mut i = 0;
while i < bytes.len() {
let c = bytes[i];
if c == b'&' {
out.push_str(matched);
i += 1;
continue;
}
if c == b'\\' {
let start = i;
while i < bytes.len() && bytes[i] == b'\\' {
i += 1;
}
let run = i - start;
if i < bytes.len() && bytes[i] == b'&' {
let pairs = run / 2;
out.extend(std::iter::repeat_n('\\', pairs));
if run % 2 == 0 {
out.push_str(matched);
} else {
out.push('&');
}
i += 1; } else {
out.extend(std::iter::repeat_n('\\', run));
}
continue;
}
let rest = &repl[i..];
let ch = rest.chars().next().unwrap();
out.push(ch);
i += ch.len_utf8();
}
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 mut re_b = regex::RegexBuilder::new(fp);
re_b.case_insensitive(rt.ignore_case_flag());
re_b.dot_matches_new_line(true);
let re = re_b.build().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 default_fmt = "%a %b %e %H:%M:%S %Z %Y";
let (fmt, ts, utc) = match args.len() {
0 => (default_fmt.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))
}
#[allow(dead_code)]
pub fn awk_mktime(s: &str) -> f64 {
awk_mktime_with_utc(s, false)
}
pub fn awk_mktime_with_utc(s: &str, utc: bool) -> 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,
};
if utc {
match Utc.from_local_datetime(&naive) {
LocalResult::Single(dt) => dt.timestamp() as f64,
LocalResult::Ambiguous(_, _) | LocalResult::None => -1.0,
}
} else {
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;
}
let first = t.as_bytes()[0];
if !matches!(first, b'+' | b'-' | b'.' | b'0'..=b'9') {
return 0.0;
}
let unsigned_hex_or_octal = !matches!(first, b'+' | b'-');
if unsigned_hex_or_octal {
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);
}
}
if let Some(prefix) = crate::runtime::longest_f64_prefix(t) {
if let Ok(v) = prefix.parse::<f64>() {
return v;
}
}
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)
}
}
#[allow(dead_code)]
pub fn awk_value_sort_cmp(a: &Value, b: &Value) -> Ordering {
awk_value_sort_cmp_with_case(a, b, false)
}
pub fn awk_value_sort_cmp_with_case(a: &Value, b: &Value, ignore_case: bool) -> 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);
}
if ignore_case {
let sa = a.as_str().to_lowercase();
let sb = b.as_str().to_lowercase();
return locale_str_cmp_sort(&sa, &sb);
}
locale_str_cmp_sort(&a.as_str(), &b.as_str())
}
pub fn asort(rt: &mut Runtime, src: &str, dest: Option<&str>) -> Result<f64> {
if src.is_empty() {
return Err(Error::Runtime(
"0 is invalid as number of arguments for asort".into(),
));
}
let ic = rt.ignore_case_flag();
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(),
None => Vec::new(),
_ => return Err(Error::Runtime(format!("asort: `{src}` is not an array"))),
};
pairs.sort_by(|(_, va), (_, vb)| awk_value_sort_cmp_with_case(va, vb, ic));
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> {
if src.is_empty() {
return Err(Error::Runtime(
"0 is invalid as number of arguments for asorti".into(),
));
}
let ic = rt.ignore_case_flag();
let mut keys: Vec<String> = match rt.get_global_var(src) {
Some(Value::Array(a)) => a.keys().cloned().collect(),
None => Vec::new(),
_ => return Err(Error::Runtime(format!("asorti: `{src}` is not an array"))),
};
keys.sort_by(|a, b| {
if ic {
locale_str_cmp_sort(&a.to_lowercase(), &b.to_lowercase())
} else {
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 => "untyped",
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("untyped"),
_ => "untyped",
}
}
#[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::{AwkMap, 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"), "untyped");
}
#[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), "untyped");
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"), "untyped");
assert_eq!(awk_typeof_array_elem(&rt, "not_array", "k"), "untyped");
}
#[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 n = asorti(&mut rt, "missing", None).unwrap();
assert_eq!(n, 0.0);
rt.vars
.insert("scalar".into(), crate::runtime::Value::Num(5.0));
let e = asorti(&mut rt, "scalar", 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_backref_expands_in_every_match() {
let mut rt = Runtime::new();
let r = super::awk_gensub(
&mut rt,
"(.)",
"[\\1]",
&Value::Str("g".into()),
Some("abc".into()),
)
.unwrap();
assert_eq!(r, "[a][b][c]");
}
#[test]
fn awk_gensub_global_backref_swap_groups_in_every_match() {
let mut rt = Runtime::new();
let r = super::awk_gensub(
&mut rt,
"(a)(b)",
"\\2\\1",
&Value::Str("g".into()),
Some("abab".into()),
)
.unwrap();
assert_eq!(r, "baba");
}
#[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_treated_as_one_like_gawk() {
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_y9z");
}
#[test]
fn awk_gensub_numeric_negative_treated_as_one_like_gawk() {
let mut rt = Runtime::new();
let out = awk_gensub(
&mut rt,
"[0-9]",
"_",
&Value::Num(-3.0),
Some("z9y9z".into()),
)
.unwrap();
assert_eq!(out, "z_y9z");
}
#[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_and_clears_complementary_bits() {
assert_eq!(super::awk_and(0xFF as f64, 0x0F as f64), 0x0F as f64);
assert_eq!(super::awk_and(0xFF as f64, 0x00 as f64), 0.0);
}
#[test]
fn awk_or_sets_all_bits() {
assert_eq!(super::awk_or(0xF0 as f64, 0x0F as f64), 0xFF as f64);
}
#[test]
fn awk_xor_toggles_bits() {
assert_eq!(super::awk_xor(0xFF as f64, 0x0F as f64), 0xF0 as f64);
assert_eq!(super::awk_xor(0xAA as f64, 0xAA as f64), 0.0);
}
#[test]
fn awk_lshift_shifts_left() {
assert_eq!(super::awk_lshift(1.0, 4.0), 16.0);
assert_eq!(super::awk_lshift(1.0, 0.0), 1.0);
}
#[test]
fn awk_rshift_shifts_right() {
assert_eq!(super::awk_rshift(16.0, 4.0), 1.0);
assert_eq!(super::awk_rshift(255.0, 1.0), 127.0);
}
#[test]
fn awk_shift_count_masked_to_six_bits() {
assert_eq!(super::awk_lshift(1.0, 64.0), 1.0);
assert_eq!(super::awk_rshift(4.0, 64.0), 4.0);
}
#[test]
fn awk_compl_flips_all_bits() {
assert_eq!(super::awk_compl(0.0), -1.0);
assert_eq!(super::awk_compl(1.0), -2.0);
}
#[test]
fn awk_and_zero_with_anything_is_zero() {
assert_eq!(super::awk_and(0.0, 0xFFFF_FFFF_FFFF_FFFF_u64 as f64), 0.0);
}
#[test]
fn awk_gensub_negative_numeric_how_treated_as_one_like_gawk() {
let mut rt = Runtime::new();
let out = awk_gensub(&mut rt, "a", "X", &Value::Num(-1.0), Some("aaa".into())).unwrap();
assert_eq!(out, "Xaa");
}
#[test]
fn strtonum_various_formats() {
assert_eq!(awk_strtonum("0x10"), 16.0);
assert_eq!(awk_strtonum("010"), 8.0);
assert_eq!(awk_strtonum("42"), 42.0);
assert_eq!(awk_strtonum("0xG"), 0.0); assert_eq!(awk_strtonum("09"), 0.0); assert_eq!(awk_strtonum(" 0x10 "), 16.0); assert_eq!(awk_strtonum("1e3"), 1000.0); assert_eq!(awk_strtonum("\t\n 42 \r"), 42.0); }
#[test]
fn gsub_overlapping_matches_behavior() {
let mut rt = rt_with_fs();
let mut s = "aaa".to_string();
let n = gsub(&mut rt, "aa", "X", Some(&mut s)).unwrap();
assert_eq!(n, 1.0);
assert_eq!(s, "Xa");
}
#[test]
fn gensub_backreferences() {
let mut rt = Runtime::new();
let s = awk_gensub(
&mut rt,
"([a-z])([0-9])",
"\\2\\1",
&Value::Str("g".into()),
Some("a1b2".into()),
)
.unwrap();
assert_eq!(s.as_str(), "1a2b");
}
#[test]
fn awk_strftime_formatting_width() {
let ts = 1672531200.0;
let fmt = Value::Str("%Y".into());
let t = Value::Num(ts);
let utc = Value::Num(1.0);
let v = awk_strftime(&[fmt, t, utc]).unwrap();
assert_eq!(v.as_str(), "2023");
}
#[test]
fn asort_preserves_numeric_values() {
let mut rt = Runtime::new();
let mut a = AwkMap::default();
a.insert("1".into(), Value::Num(10.0));
a.insert("2".into(), Value::Num(5.0));
rt.vars.insert("a".into(), Value::Array(a));
let n = asort(&mut rt, "a", None).unwrap();
assert_eq!(n, 2.0);
assert_eq!(rt.array_get("a", "1").as_number(), 5.0);
assert_eq!(rt.array_get("a", "2").as_number(), 10.0);
}
#[test]
fn patsplit_with_capturing_groups() {
let mut rt = Runtime::new();
let n = patsplit(&mut rt, "abc 123", "a", Some("([a-z]+)|([0-9]+)"), None).unwrap();
assert_eq!(n, 2.0);
assert!(rt.array_has("a", "1"));
assert_eq!(rt.array_get("a", "1").as_str(), "abc");
assert_eq!(rt.array_get("a", "2").as_str(), "123");
}
#[test]
fn patsplit_empty_matches_behavior() {
let mut rt = Runtime::new();
let n = patsplit(&mut rt, "baac", "a", Some("a*"), None).unwrap();
assert!(n >= 1.0);
}
#[test]
fn awk_bitwise_negative_numbers() {
assert_eq!(super::awk_and(-1.0, 1.0), 1.0);
assert_eq!(super::awk_or(-1.0, 0.0), -1.0);
assert_eq!(super::awk_xor(-1.0, -1.0), 0.0);
}
#[test]
fn mktime_invalid_format_returns_minus_one() {
assert_eq!(awk_mktime("2023 13 01 00 00 00"), -1.0); assert_eq!(awk_mktime("2023 01 32 00 00 00"), -1.0); }
#[test]
fn strftime_utc_vs_local() {
let ts = 1672531200.0; let fmt = Value::Str("%Y-%m-%d %H:%M:%S".into());
let t = Value::Num(ts);
let v_utc = awk_strftime(&[fmt.clone(), t.clone(), Value::Num(1.0)]).unwrap();
assert_eq!(v_utc.as_str(), "2023-01-01 00:00:00");
}
#[test]
fn asort_empty_array() {
let mut rt = Runtime::new();
rt.array_delete("a", None);
rt.vars
.insert("a".into(), Value::Array(crate::runtime::AwkMap::default()));
let n = asort(&mut rt, "a", None).unwrap();
assert_eq!(n, 0.0);
}
#[test]
fn asorti_empty_array() {
let mut rt = Runtime::new();
rt.array_delete("a", None);
rt.vars
.insert("a".into(), Value::Array(crate::runtime::AwkMap::default()));
let n = asorti(&mut rt, "a", None).unwrap();
assert_eq!(n, 0.0);
}
#[test]
fn typeof_builtin_logic() {
assert_eq!(awk_typeof_value(&Value::Num(1.0)), "number");
assert_eq!(awk_typeof_value(&Value::Str("x".into())), "string");
assert_eq!(awk_typeof_value(&Value::Regexp("a".into())), "regexp");
assert_eq!(awk_typeof_value(&Value::Uninit), "untyped");
assert_eq!(
awk_typeof_value(&Value::Array(crate::runtime::AwkMap::default())),
"array"
);
}
#[test]
fn awk_strftime_complex_format() {
let ts = 1672531200.0;
let fmt = Value::Str("Day %j of %Y, %H:%M:%S".into());
let t = Value::Num(ts);
let utc = Value::Num(1.0);
let v = awk_strftime(&[fmt, t, utc]).unwrap();
assert_eq!(v.as_str(), "Day 001 of 2023, 00:00:00");
}
#[test]
fn awk_split_regex_fs_captures() {
let mut rt = Runtime::new();
let n = patsplit(&mut rt, "a1b22c", "a", Some("[0-9]+"), None).unwrap();
assert_eq!(n, 2.0);
assert_eq!(rt.array_get("a", "1").as_str(), "1");
assert_eq!(rt.array_get("a", "2").as_str(), "22");
}
#[test]
fn asort_basic_v4() {
let mut rt = Runtime::new();
rt.array_set("a", "1".into(), Value::Str("z".into()));
rt.array_set("a", "2".into(), Value::Str("a".into()));
let n = asort(&mut rt, "a", Some("b")).unwrap();
assert_eq!(n, 2.0);
assert_eq!(rt.array_get("b", "1").as_str(), "a");
assert_eq!(rt.array_get("b", "2").as_str(), "z");
}
#[test]
fn awk_strftime_no_args_v3() {
let v = awk_strftime(&[]).unwrap();
assert!(!v.as_str().is_empty());
}
#[test]
fn awk_strtonum_scientific_v3() {
assert_eq!(awk_strtonum("1e3"), 1000.0);
assert_eq!(awk_strtonum("0x10"), 16.0);
}
#[test]
fn awk_bitwise_direct_v2() {
assert_eq!(super::awk_and(255.0, 15.0), 15.0);
assert_eq!(super::awk_or(240.0, 15.0), 255.0);
assert_eq!(super::awk_xor(255.0, 15.0), 240.0);
assert_eq!(super::awk_lshift(1.0, 4.0), 16.0);
assert_eq!(super::awk_rshift(16.0, 4.0), 1.0);
assert_eq!(super::awk_compl(0.0), -1.0);
}
#[test]
fn gsub_literal_eligible_v2() {
assert!(super::gsub_literal_eligible("abc", "def"));
assert!(!super::gsub_literal_eligible("a.c", "def"));
assert!(!super::gsub_literal_eligible("abc", "d&f"));
}
#[test]
fn awk_typeof_array_elem_untyped_v2() {
let rt = Runtime::new();
assert_eq!(
super::awk_typeof_array_elem(&rt, "nonexistent", "key"),
"untyped"
);
}
#[test]
fn awk_is_literal_pattern_v2() {
assert!(super::is_literal_pattern("abc"));
assert!(!super::is_literal_pattern("a.c"));
}
#[test]
fn awk_typeof_value_v2() {
assert_eq!(super::awk_typeof_value(&Value::Uninit), "untyped");
assert_eq!(super::awk_typeof_value(&Value::Num(1.0)), "number");
assert_eq!(super::awk_typeof_value(&Value::Str("a".into())), "string");
}
#[test]
fn awk_typeof_array_v3() {
let mut rt = Runtime::new();
rt.vars.insert("a".into(), Value::Array(AwkMap::default()));
assert_eq!(super::awk_typeof_value(rt.vars.get("a").unwrap()), "array");
}
#[test]
fn awk_typeof_regexp_v3() {
assert_eq!(
super::awk_typeof_value(&Value::Regexp(".*".into())),
"regexp"
);
}
#[test]
fn awk_and_large_values_v3() {
assert_eq!(
super::awk_and(0xFFFFFFFFu64 as f64, 0x0000000Fu64 as f64),
15.0
);
}
#[test]
fn awk_or_large_values_v3() {
assert_eq!(
super::awk_or(0xF0000000u64 as f64, 0x0F000000u64 as f64),
0xFF000000u64 as f64
);
}
#[test]
fn awk_xor_large_values_v3() {
assert_eq!(
super::awk_xor(0xFFFFFFFFu64 as f64, 0x0F0F0F0Fu64 as f64),
0xF0F0F0F0u64 as f64
);
}
#[test]
fn awk_lshift_large_v3() {
assert_eq!(super::awk_lshift(1.0, 32.0), 4294967296.0);
}
#[test]
fn awk_rshift_large_v3() {
assert_eq!(super::awk_rshift(4294967296.0, 32.0), 1.0);
}
#[test]
fn gsub_backrefs_v2() {
let mut rt = Runtime::new();
let mut s = "aabb".to_string();
let n = super::gsub(&mut rt, "a", "x&y", Some(&mut s)).unwrap();
assert_eq!(n, 2.0);
assert_eq!(s, "xayxaybb");
}
#[test]
fn patsplit_basic_v2() {
let mut rt = Runtime::new();
let n = super::patsplit(&mut rt, "a1b22c", "a", Some("[0-9]+"), None).unwrap();
assert_eq!(n, 2.0);
assert_eq!(rt.array_get("a", "1").as_str(), "1");
assert_eq!(rt.array_get("a", "2").as_str(), "22");
}
#[test]
fn awk_strtonum_octal_v4() {
assert_eq!(super::awk_strtonum("010"), 8.0);
}
#[test]
fn awk_strtonum_invalid_hex_v4() {
assert_eq!(super::awk_strtonum("0xG"), 0.0);
}
#[test]
fn awk_strtonum_empty_v4() {
assert_eq!(super::awk_strtonum(""), 0.0);
}
#[test]
fn awk_strtonum_ws_v4() {
assert_eq!(super::awk_strtonum(" 123 "), 123.0);
}
#[test]
fn awk_and_v10() {
assert_eq!(super::awk_and(1.0, 1.0), 1.0);
}
#[test]
fn awk_or_v10() {
assert_eq!(super::awk_or(1.0, 0.0), 1.0);
}
#[test]
fn awk_xor_v10() {
assert_eq!(super::awk_xor(1.0, 1.0), 0.0);
}
#[test]
fn awk_compl_v10() {
assert_eq!(super::awk_compl(-1.0), 0.0);
}
#[test]
fn awk_lshift_v10() {
assert_eq!(super::awk_lshift(1.0, 1.0), 2.0);
}
#[test]
fn awk_rshift_v10() {
assert_eq!(super::awk_rshift(2.0, 1.0), 1.0);
}
#[test]
fn awk_typeof_num_v10() {
assert_eq!(super::awk_typeof_value(&Value::Num(1.0)), "number");
}
#[test]
fn awk_typeof_str_v10() {
assert_eq!(super::awk_typeof_value(&Value::Str("a".into())), "string");
}
#[test]
fn awk_typeof_uninit_v10() {
assert_eq!(super::awk_typeof_value(&Value::Uninit), "untyped");
}
#[test]
fn awk_is_literal_v10() {
assert!(super::is_literal_pattern("abc"));
}
#[test]
fn awk_not_literal_v10() {
assert!(!super::is_literal_pattern("a*"));
}
#[test]
fn awk_gsub_eligible_v10() {
assert!(super::gsub_literal_eligible("a", "b"));
}
#[test]
fn awk_gsub_not_eligible_v10() {
assert!(!super::gsub_literal_eligible("a*", "b"));
}
#[test]
fn awk_systime_v10() {
assert!(super::awk_systime() > 0.0);
}
#[test]
fn awk_bitwise_and_v33() {
assert_eq!(super::awk_and(3.0, 1.0), 1.0);
}
#[test]
fn awk_bitwise_or_v33() {
assert_eq!(super::awk_or(2.0, 1.0), 3.0);
}
#[test]
fn awk_bitwise_xor_v33() {
assert_eq!(super::awk_xor(3.0, 1.0), 2.0);
}
#[test]
fn awk_bitwise_compl_v33() {
assert_eq!(super::awk_compl(-1.0), 0.0);
}
#[test]
fn awk_bitwise_lshift_v33() {
assert_eq!(super::awk_lshift(1.0, 1.0), 2.0);
}
#[test]
fn awk_bitwise_rshift_v33() {
assert_eq!(super::awk_rshift(2.0, 1.0), 1.0);
}
#[test]
fn gensub_full_match_backref_v10() {
let mut rt = Runtime::new();
let s = super::awk_gensub(
&mut rt,
"abc",
"x\\0y",
&Value::Str("g".into()),
Some("abc".into()),
)
.unwrap();
assert_eq!(s, "x0y");
}
#[test]
fn gensub_numbered_occurrence_v10() {
let mut rt = Runtime::new();
let s = super::awk_gensub(&mut rt, "a", "x", &Value::Num(2.0), Some("aaa".into())).unwrap();
assert_eq!(s, "axa");
}
#[test]
fn asort_numeric_strings_behavior_v10() {
let mut rt = Runtime::new();
rt.array_set("a", "1".into(), Value::Str("10".into()));
rt.array_set("a", "2".into(), Value::Str("2".into()));
let n = asort(&mut rt, "a", Some("b")).unwrap();
assert_eq!(n, 2.0);
assert_eq!(rt.array_get("b", "1").as_str(), "2");
assert_eq!(rt.array_get("b", "2").as_str(), "10");
}
#[test]
fn split_with_seps_array_v11() {
}
#[test]
fn awk_strftime_exhaustive_v12() {
let ts = 1672531200.0; let t = Value::Num(ts);
let utc = Value::Num(1.0);
let cases = [
("%Y", "2023"),
("%m", "01"),
("%d", "01"),
("%H", "00"),
("%M", "00"),
("%S", "00"),
("%y", "23"),
("%j", "001"),
("%%", "%"),
];
for (fmt_str, expected) in cases {
let fmt = Value::Str(fmt_str.into());
let v = super::awk_strftime(&[fmt, t.clone(), utc.clone()]).unwrap();
assert_eq!(v.as_str(), expected, "fmt: {}", fmt_str);
}
}
#[test]
fn strftime_v12_a() {
assert_eq!(
super::awk_strftime(&[
Value::Str("%a".into()),
Value::Num(1672531200.0),
Value::Num(1.0)
])
.unwrap()
.as_str(),
"Sun"
);
}
#[test]
fn strftime_v12_upper_a() {
assert_eq!(
super::awk_strftime(&[
Value::Str("%A".into()),
Value::Num(1672531200.0),
Value::Num(1.0)
])
.unwrap()
.as_str(),
"Sunday"
);
}
#[test]
fn strftime_v12_b() {
assert_eq!(
super::awk_strftime(&[
Value::Str("%b".into()),
Value::Num(1672531200.0),
Value::Num(1.0)
])
.unwrap()
.as_str(),
"Jan"
);
}
#[test]
fn strftime_v12_upper_b() {
assert_eq!(
super::awk_strftime(&[
Value::Str("%B".into()),
Value::Num(1672531200.0),
Value::Num(1.0)
])
.unwrap()
.as_str(),
"January"
);
}
#[test]
fn strftime_v12_u() {
assert_eq!(
super::awk_strftime(&[
Value::Str("%u".into()),
Value::Num(1672531200.0),
Value::Num(1.0)
])
.unwrap()
.as_str(),
"7"
);
}
#[test]
fn strftime_v12_w() {
assert_eq!(
super::awk_strftime(&[
Value::Str("%w".into()),
Value::Num(1672531200.0),
Value::Num(1.0)
])
.unwrap()
.as_str(),
"0"
);
}
}