#![allow(non_snake_case)]
use crate::ported::charset::{
vim_str2nr, STR2NR_BIN, STR2NR_FORCE, STR2NR_HEX, STR2NR_OCT, STR2NR_OOCT, STR2NR_QUOTE,
};
use crate::ported::eval::encode::encode_tv2string;
use crate::ported::eval::typval::{
tv_blob_alloc_ret, tv_get_number_chk, tv_get_string, tv_list_alloc_ret, tv_list_append_number,
tv_list_append_tv,
};
use crate::ported::eval::typval_defs_h::{typval_T, typval_vval_union::*, varnumber_T, VarType::*};
use crate::ported::option::get_option_value;
pub fn f_string(argvars: &[typval_T], rettv: &mut typval_T) {
rettv.v_type = VAR_STRING;
rettv.vval = v_string(encode_tv2string(&argvars[0]));
}
pub fn f_str2nr(argvars: &[typval_T], rettv: &mut typval_T) {
let base = argvars
.get(1)
.map(|t| tv_get_number_chk(t, None))
.unwrap_or(10);
if !matches!(base, 2 | 8 | 10 | 16) {
return; }
let mut what = match base {
2 => STR2NR_BIN | STR2NR_FORCE,
8 => STR2NR_OCT | STR2NR_OOCT | STR2NR_FORCE,
16 => STR2NR_HEX | STR2NR_FORCE,
_ => 0,
};
if argvars
.get(2)
.is_some_and(|t| tv_get_number_chk(t, None) != 0)
{
what |= STR2NR_QUOTE;
}
let s = tv_get_string(&argvars[0]);
let p = s.trim_start();
let (neg, p) = match p.strip_prefix('-') {
Some(rest) => (true, rest.trim_start()),
None => (false, p.strip_prefix('+').map(str::trim_start).unwrap_or(p)),
};
let mut n: varnumber_T = 0;
vim_str2nr(p, None, None, what, Some(&mut n), None, 0, false, None);
rettv.vval = v_number(if neg { -n } else { n });
}
pub fn f_strlen(argvars: &[typval_T], rettv: &mut typval_T) {
rettv.vval = v_number(tv_get_string(&argvars[0]).len() as varnumber_T);
}
pub fn f_tolower(argvars: &[typval_T], rettv: &mut typval_T) {
rettv.v_type = VAR_STRING;
rettv.vval = v_string(tv_get_string(&argvars[0]).to_lowercase());
}
pub fn f_toupper(argvars: &[typval_T], rettv: &mut typval_T) {
rettv.v_type = VAR_STRING;
rettv.vval = v_string(tv_get_string(&argvars[0]).to_uppercase());
}
pub fn f_strchars(argvars: &[typval_T], rettv: &mut typval_T) {
let s = tv_get_string(&argvars[0]);
let skipcc = argvars
.get(1)
.is_some_and(|t| tv_get_number_chk(t, None) != 0);
let n = if skipcc {
s.chars().filter(|c| !utf_iscomposing(*c)).count()
} else {
s.chars().count()
};
rettv.vval = v_number(n as varnumber_T);
}
pub fn f_strpart(argvars: &[typval_T], rettv: &mut typval_T) {
let s = tv_get_string(&argvars[0]);
let bytes = s.as_bytes();
let slen = bytes.len() as varnumber_T;
let mut nbyte = tv_get_number_chk(&argvars[1], None);
let mut len = if argvars.len() >= 3 {
tv_get_number_chk(&argvars[2], None)
} else {
slen - nbyte
};
if nbyte < 0 {
len += nbyte;
nbyte = 0;
} else if nbyte > slen {
nbyte = slen;
}
if len < 0 {
len = 0;
} else if nbyte + len > slen {
len = slen - nbyte;
}
if argvars.len() >= 3 && argvars.get(3).is_some_and(|t| t.v_type != VAR_UNKNOWN) {
let mut off = nbyte;
while off < slen && len > 0 {
off += crate::ported::mbyte::utf_ptr2len(&bytes[off as usize..]) as varnumber_T;
len -= 1;
}
len = off - nbyte;
}
let start = nbyte as usize;
let end = start + len as usize;
rettv.v_type = VAR_STRING;
rettv.vval = v_string(String::from_utf8_lossy(&bytes[start..end]).into_owned());
}
pub fn f_stridx(argvars: &[typval_T], rettv: &mut typval_T) {
let hay = tv_get_string(&argvars[0]);
let needle = tv_get_string(&argvars[1]);
let start = argvars
.get(2)
.map_or(0, |t| tv_get_number_chk(t, None).max(0) as usize);
let idx = if start <= hay.len() {
hay[start..]
.find(&needle)
.map(|i| (i + start) as varnumber_T)
} else {
None
};
rettv.vval = v_number(idx.unwrap_or(-1));
}
pub fn f_trim(argvars: &[typval_T], rettv: &mut typval_T) {
let s = tv_get_string(&argvars[0]);
rettv.v_type = VAR_STRING;
let has_mask = argvars.len() >= 2 && argvars[1].v_type != VAR_UNKNOWN;
let mask: Vec<char> = if has_mask {
tv_get_string(&argvars[1]).chars().collect()
} else {
vec![' ', '\t', '\r', '\n', '\u{0b}', '\u{0c}']
};
let pred = |c: char| mask.contains(&c);
let dir = argvars
.get(2)
.filter(|t| t.v_type != VAR_UNKNOWN)
.map_or(0, |t| tv_get_number_chk(t, None));
let trimmed = match dir {
1 => s.trim_start_matches(pred),
2 => s.trim_end_matches(pred),
_ => s.trim_matches(pred),
};
rettv.vval = v_string(trimmed.to_string());
}
pub fn f_strridx(argvars: &[typval_T], rettv: &mut typval_T) {
let hay = tv_get_string(&argvars[0]);
let needle = tv_get_string(&argvars[1]);
rettv.v_type = VAR_NUMBER;
rettv.vval = v_number(-1);
let hb = hay.as_bytes();
let nb = needle.as_bytes();
let end_idx: isize = if argvars.len() > 2 && argvars[2].v_type != VAR_UNKNOWN {
let e = tv_get_number_chk(&argvars[2], None) as isize;
if e < 0 {
return;
}
e
} else {
hb.len() as isize
};
let lastmatch: Option<usize> = if nb.is_empty() {
Some(end_idx as usize)
} else {
let mut found = None;
let mut from = 0usize;
loop {
match (from..=hb.len().saturating_sub(nb.len())).find(|&i| hb[i..i + nb.len()] == *nb) {
Some(pos) if (pos as isize) <= end_idx => {
found = Some(pos);
from = pos + 1;
}
_ => break,
}
}
found
};
if let Some(i) = lastmatch {
rettv.vval = v_number(i as varnumber_T);
}
}
pub fn f_tr(argvars: &[typval_T], rettv: &mut typval_T) {
let src = tv_get_string(&argvars[0]);
let fromstr = tv_get_string(&argvars[1]);
let from: Vec<char> = fromstr.chars().collect();
let to: Vec<char> = tv_get_string(&argvars[2]).chars().collect();
rettv.v_type = VAR_STRING;
let mut out = String::new();
for c in src.chars() {
match from.iter().position(|&f| f == c) {
Some(i) => match to.get(i) {
Some(&t) => out.push(t),
None => {
crate::ported::message::semsg(&format!("E475: Invalid argument: {fromstr}"));
rettv.vval = v_string(String::new());
return;
}
},
None => out.push(c),
}
}
rettv.vval = v_string(out);
}
pub fn f_str2list(argvars: &[typval_T], rettv: &mut typval_T) {
let s = tv_get_string(&argvars[0]);
let l = tv_list_alloc_ret(rettv, s.chars().count() as isize);
let mut lb = l.borrow_mut();
for c in s.chars() {
tv_list_append_number(&mut lb, c as varnumber_T);
}
}
pub fn f_strgetchar(argvars: &[typval_T], rettv: &mut typval_T) {
let s = tv_get_string(&argvars[0]);
let idx = tv_get_number_chk(&argvars[1], None);
rettv.vval = v_number(if idx < 0 {
-1
} else {
s.chars().nth(idx as usize).map_or(-1, |c| c as varnumber_T)
});
}
pub fn f_strcharpart(argvars: &[typval_T], rettv: &mut typval_T) {
let skipcc = argvars
.get(3)
.is_some_and(|t| tv_get_number_chk(t, None) != 0);
let mut units: Vec<String> = Vec::new();
for c in tv_get_string(&argvars[0]).chars() {
if skipcc && utf_iscomposing(c) && !units.is_empty() {
units.last_mut().unwrap().push(c);
} else {
units.push(c.to_string());
}
}
let mut start = tv_get_number_chk(&argvars[1], None);
let has_len = argvars.len() >= 3;
let mut len = if has_len {
tv_get_number_chk(&argvars[2], None)
} else {
units.len() as varnumber_T - start
};
if start < 0 {
len += start; start = 0;
}
let start = (start as usize).min(units.len());
let len = len.max(0) as usize;
let end = (start + len).min(units.len());
rettv.v_type = VAR_STRING;
rettv.vval = v_string(units[start..end].concat());
}
pub fn f_byteidx(argvars: &[typval_T], rettv: &mut typval_T) {
rettv.vval = v_number(byteidx_impl(argvars, true));
}
fn byteidx_impl(argvars: &[typval_T], skipcc: bool) -> varnumber_T {
let s = tv_get_string(&argvars[0]);
let nr = tv_get_number_chk(&argvars[1], None);
if nr < 0 {
return -1;
}
let nr = nr as usize;
let mut count = 0usize;
let mut prev = false; for (b, c) in s.char_indices() {
let folds = skipcc && prev && utf_iscomposing(c);
if !folds {
if count == nr {
return b as varnumber_T;
}
count += 1;
}
prev = true;
}
if nr == count {
s.len() as varnumber_T
} else {
-1
}
}
pub fn f_charidx(argvars: &[typval_T], rettv: &mut typval_T) {
let s = tv_get_string(&argvars[0]);
let idx = tv_get_number_chk(&argvars[1], None);
if idx < 0 {
rettv.vval = v_number(-1);
return;
}
let countcc = argvars.len() >= 3
&& argvars[2].v_type != VAR_UNKNOWN
&& tv_get_number_chk(&argvars[2], None) != 0;
let use_utf16 = argvars.len() >= 4
&& argvars[3].v_type != VAR_UNKNOWN
&& tv_get_number_chk(&argvars[3], None) != 0;
let mut units: Vec<(usize, varnumber_T)> = Vec::new();
let mut u16_acc: varnumber_T = 0;
let mut prev = false;
for (b, c) in s.char_indices() {
let folds = !countcc && prev && utf_iscomposing(c);
prev = true;
if !folds {
units.push((b, u16_acc));
u16_acc += utf_char2utf16len(c);
}
}
let n = units.len() as varnumber_T;
let result = if use_utf16 {
match idx {
i if i == u16_acc => n,
i if i > u16_acc => -1,
i => units.iter().take_while(|(_, before)| *before <= i).count() as varnumber_T - 1,
}
} else {
let strlen = s.len() as varnumber_T;
match idx {
i if i == strlen => n,
i if i > strlen => -1,
i => {
units
.iter()
.take_while(|(sb, _)| (*sb as varnumber_T) <= i)
.count() as varnumber_T
- 1
}
}
};
rettv.vval = v_number(result);
}
pub fn f_byteidxcomp(argvars: &[typval_T], rettv: &mut typval_T) {
rettv.vval = v_number(byteidx_impl(argvars, false));
}
pub(crate) fn utf_iscomposing(c: char) -> bool {
let u = c as u32;
matches!(u,
0x0300..=0x036F | 0x0483..=0x0489 | 0x0591..=0x05BD | 0x05BF
| 0x0610..=0x061A | 0x064B..=0x065F | 0x0670 | 0x06D6..=0x06DC
| 0x06DF..=0x06E4 | 0x0711 | 0x0730..=0x074A | 0x07A6..=0x07B0
| 0x0816..=0x0823 | 0x1AB0..=0x1AFF | 0x1DC0..=0x1DFF
| 0x20D0..=0x20FF | 0xFE20..=0xFE2F)
}
pub fn f_strcharlen(argvars: &[typval_T], rettv: &mut typval_T) {
let n = tv_get_string(&argvars[0])
.chars()
.filter(|c| !utf_iscomposing(*c))
.count();
rettv.vval = v_number(n as varnumber_T);
}
pub fn f_strtrans(argvars: &[typval_T], rettv: &mut typval_T) {
let s = tv_get_string(&argvars[0]);
let mut out = String::with_capacity(s.len());
for c in s.chars() {
let u = c as u32;
if u < 0x20 {
out.push('^');
out.push((u as u8 + 0x40) as char);
} else if u == 0x7F {
out.push_str("^?");
} else {
out.push(c);
}
}
rettv.v_type = VAR_STRING;
rettv.vval = v_string(out);
}
pub fn f_slice(argvars: &[typval_T], rettv: &mut typval_T) {
let len: varnumber_T = match (argvars[0].v_type, &argvars[0].vval) {
(VAR_LIST, v_list(Some(l))) => l.borrow().lv_items.len() as varnumber_T,
(VAR_BLOB, v_blob(Some(b))) => b.borrow().bv_ga.len() as varnumber_T,
(VAR_LIST, _) | (VAR_BLOB, _) => 0,
_ => tv_get_string(&argvars[0]).chars().count() as varnumber_T,
};
let clamp = |mut i: varnumber_T| -> varnumber_T {
if i < 0 {
i += len;
}
i.clamp(0, len)
};
let s = clamp(tv_get_number_chk(&argvars[1], None));
let has_end = argvars.len() >= 3 && argvars[2].v_type != VAR_UNKNOWN;
let mut e = if has_end {
clamp(tv_get_number_chk(&argvars[2], None))
} else {
len
};
if e < s {
e = s;
}
let (s, e) = (s as usize, e as usize);
match (argvars[0].v_type, &argvars[0].vval) {
(VAR_LIST, v_list(l)) => {
let out = tv_list_alloc_ret(rettv, 0);
if let Some(l) = l {
let lb = l.borrow();
let mut ob = out.borrow_mut();
for it in &lb.lv_items[s..e] {
tv_list_append_tv(&mut ob, it.li_tv.clone());
}
}
}
(VAR_BLOB, v_blob(b)) => {
let out = tv_blob_alloc_ret(rettv);
if let Some(b) = b {
out.borrow_mut()
.bv_ga
.extend_from_slice(&b.borrow().bv_ga[s..e]);
}
}
_ => {
let chars: Vec<char> = tv_get_string(&argvars[0]).chars().collect();
rettv.v_type = VAR_STRING;
rettv.vval = v_string(chars[s..e].iter().collect());
}
}
}
fn utf_char2cells(c: char) -> usize {
if utf_iscomposing(c) {
return 0;
}
let u = c as u32;
if let Some(w) = cw_value(u) {
return w;
}
let wide = matches!(u,
0x1100..=0x115F | 0x2329 | 0x232A | 0x2E80..=0x303E | 0x3041..=0x33FF
| 0x3400..=0x4DBF | 0x4E00..=0x9FFF | 0xA000..=0xA4CF | 0xAC00..=0xD7A3
| 0xF900..=0xFAFF | 0xFE30..=0xFE4F | 0xFF00..=0xFF60 | 0xFFE0..=0xFFE6
| 0x1F300..=0x1FAFF | 0x20000..=0x3FFFD);
if wide {
2
} else {
1
}
}
thread_local! {
static CW_TABLE: std::cell::RefCell<Vec<(u32, u32, u8)>> =
const { std::cell::RefCell::new(Vec::new()) };
}
fn cw_value(c: u32) -> Option<usize> {
CW_TABLE.with(|t| {
t.borrow()
.iter()
.find(|(lo, hi, _)| c >= *lo && c <= *hi)
.map(|(_, _, w)| *w as usize)
})
}
pub fn f_setcellwidths(argvars: &[typval_T], _rettv: &mut typval_T) {
let l = match (argvars[0].v_type, &argvars[0].vval) {
(VAR_LIST, v_list(Some(l))) => l.clone(),
_ => {
crate::ported::message::emsg("E1109: List required");
return;
}
};
let mut table: Vec<(u32, u32, u8)> = Vec::new();
for item in l.borrow().lv_items.iter() {
let triple: Vec<varnumber_T> = match (item.li_tv.v_type, &item.li_tv.vval) {
(VAR_LIST, v_list(Some(inner))) => inner
.borrow()
.lv_items
.iter()
.map(|e| crate::ported::eval::typval::tv_get_number(&e.li_tv))
.collect(),
_ => {
crate::ported::message::emsg("E1110: List item is not a List");
return;
}
};
if triple.len() != 3 {
crate::ported::message::emsg("E1111: List with three Numbers required");
return;
}
let (lo, hi, w) = (triple[0], triple[1], triple[2]);
if w != 1 && w != 2 {
crate::ported::message::emsg("E1112: List item width must be 1 or 2");
return;
}
if lo < 0 || hi < lo {
crate::ported::message::emsg("E1113: Overlapping ranges for 0x...");
return;
}
table.push((lo as u32, hi as u32, w as u8));
}
CW_TABLE.with(|t| *t.borrow_mut() = table);
}
pub fn f_getcellwidths(_argvars: &[typval_T], rettv: &mut typval_T) {
let table = CW_TABLE.with(|t| t.borrow().clone());
let out = tv_list_alloc_ret(rettv, table.len() as isize);
let mut ob = out.borrow_mut();
for (lo, hi, w) in table {
let inner = crate::ported::eval::typval::tv_list_alloc(3);
{
let mut ib = inner.borrow_mut();
tv_list_append_number(&mut ib, lo as varnumber_T);
tv_list_append_number(&mut ib, hi as varnumber_T);
tv_list_append_number(&mut ib, w as varnumber_T);
}
tv_list_append_tv(
&mut ob,
typval_T {
v_type: VAR_LIST,
v_lock: crate::ported::eval::typval_defs_h::VarLockStatus::VAR_UNLOCKED,
vval: v_list(Some(inner)),
},
);
}
}
pub fn f_strwidth(argvars: &[typval_T], rettv: &mut typval_T) {
let w: usize = tv_get_string(&argvars[0]).chars().map(utf_char2cells).sum();
rettv.vval = v_number(w as varnumber_T);
}
pub fn f_strdisplaywidth(argvars: &[typval_T], rettv: &mut typval_T) {
let s = tv_get_string(&argvars[0]);
let col0 = if argvars.len() >= 2 {
tv_get_number_chk(&argvars[1], None).max(0) as usize
} else {
0
};
let ts = {
let t = tv_get_number_chk(&get_option_value("tabstop"), None);
if t > 0 {
t as usize
} else {
8
}
};
let mut col = col0;
for c in s.chars() {
if c == '\t' {
col += ts - (col % ts);
} else {
col += utf_char2cells(c);
}
}
rettv.vval = v_number((col - col0) as varnumber_T);
}
pub fn f_charclass(argvars: &[typval_T], rettv: &mut typval_T) {
let class = match tv_get_string(&argvars[0]).chars().next() {
None => 0,
Some(c) => utf_class_tab(c as u32),
};
rettv.vval = v_number(class);
}
fn utf_class_tab(c: u32) -> varnumber_T {
if c < 0x100 {
if c == b' ' as u32 || c == b'\t' as u32 || c == 0 || c == 0xa0 {
return 0; }
if (c as u8).is_ascii_alphanumeric() || c == b'_' as u32 || (0xc0..=0xff).contains(&c) {
return 2; }
return 1; }
if matches!(c, 0x1F1E6..=0x1F1FF | 0x1F300..=0x1FAFF | 0x2600..=0x27BF) {
return 3;
}
const CLASSES: &[(u32, u32, varnumber_T)] = &[
(0x037e, 0x037e, 1), (0x0387, 0x0387, 1), (0x055a, 0x055f, 1), (0x0589, 0x0589, 1), (0x05be, 0x05be, 1),
(0x05c0, 0x05c0, 1),
(0x05c3, 0x05c3, 1),
(0x05f3, 0x05f4, 1),
(0x060c, 0x060c, 1),
(0x061b, 0x061b, 1),
(0x061f, 0x061f, 1),
(0x066a, 0x066d, 1),
(0x06d4, 0x06d4, 1),
(0x0700, 0x070d, 1), (0x0964, 0x0965, 1),
(0x0970, 0x0970, 1),
(0x0df4, 0x0df4, 1),
(0x0e4f, 0x0e4f, 1),
(0x0e5a, 0x0e5b, 1),
(0x0f04, 0x0f12, 1),
(0x0f3a, 0x0f3d, 1),
(0x0f85, 0x0f85, 1),
(0x104a, 0x104f, 1), (0x10fb, 0x10fb, 1), (0x1361, 0x1368, 1), (0x166d, 0x166e, 1), (0x1680, 0x1680, 0),
(0x169b, 0x169c, 1),
(0x16eb, 0x16ed, 1),
(0x1735, 0x1736, 1),
(0x17d4, 0x17dc, 1), (0x1800, 0x180a, 1), (0x2000, 0x200b, 0), (0x200c, 0x2027, 1), (0x2028, 0x2029, 0),
(0x202a, 0x202e, 1), (0x202f, 0x202f, 0),
(0x2030, 0x205e, 1), (0x205f, 0x205f, 0),
(0x2060, 0x206f, 1), (0x2070, 0x207f, 0x2070), (0x2080, 0x2094, 0x2080), (0x20a0, 0x27ff, 1), (0x2800, 0x28ff, 0x2800), (0x2900, 0x2998, 1), (0x29d8, 0x29db, 1),
(0x29fc, 0x29fd, 1),
(0x2e00, 0x2e7f, 1), (0x3000, 0x3000, 0), (0x3001, 0x3020, 1), (0x3030, 0x3030, 1),
(0x303d, 0x303d, 1),
(0x3040, 0x309f, 0x3040), (0x30a0, 0x30ff, 0x30a0), (0x3300, 0x9fff, 0x4e00), (0xac00, 0xd7a3, 0xac00), (0xf900, 0xfaff, 0x4e00), (0xfd3e, 0xfd3f, 1),
(0xfe30, 0xfe6b, 1), (0xff00, 0xff0f, 1), (0xff1a, 0xff20, 1), (0xff3b, 0xff40, 1), (0xff5b, 0xff65, 1), (0x1d000, 0x1d24f, 1), (0x1d400, 0x1d7ff, 1), (0x1f000, 0x1f2ff, 1), (0x1f300, 0x1f9ff, 1), (0x20000, 0x2a6df, 0x4e00), (0x2a700, 0x2b73f, 0x4e00), (0x2b740, 0x2b81f, 0x4e00), (0x2f800, 0x2fa1f, 0x4e00), ];
let mut bot: i64 = 0;
let mut top: i64 = CLASSES.len() as i64 - 1;
while top >= bot {
let mid = ((bot + top) / 2) as usize;
if CLASSES[mid].1 < c {
bot = mid as i64 + 1;
} else if CLASSES[mid].0 > c {
top = mid as i64 - 1;
} else {
return CLASSES[mid].2;
}
}
2
}
fn utf_char2utf16len(c: char) -> varnumber_T {
if c as u32 > 0xFFFF {
2
} else {
1
}
}
pub fn f_strutf16len(argvars: &[typval_T], rettv: &mut typval_T) {
let countcc = argvars.len() >= 2
&& argvars[1].v_type != VAR_UNKNOWN
&& tv_get_number_chk(&argvars[1], None) != 0;
let n: varnumber_T = tv_get_string(&argvars[0])
.chars()
.filter(|c| countcc || !utf_iscomposing(*c))
.map(utf_char2utf16len)
.sum();
rettv.vval = v_number(n);
}
pub fn f_utf16idx(argvars: &[typval_T], rettv: &mut typval_T) {
let s = tv_get_string(&argvars[0]);
let idx = tv_get_number_chk(&argvars[1], None);
if idx < 0 {
rettv.vval = v_number(-1);
return;
}
let countcc = argvars.len() >= 3
&& argvars[2].v_type != VAR_UNKNOWN
&& tv_get_number_chk(&argvars[2], None) != 0;
let usecharidx = argvars.len() >= 4
&& argvars[3].v_type != VAR_UNKNOWN
&& tv_get_number_chk(&argvars[3], None) != 0;
let mut units: Vec<(usize, varnumber_T)> = Vec::new();
let mut u16_acc: varnumber_T = 0;
let mut prev = false;
for (b, c) in s.char_indices() {
let folds = !countcc && prev && utf_iscomposing(c);
prev = true;
if !folds {
units.push((b, u16_acc));
u16_acc += utf_char2utf16len(c);
}
}
let total_u16 = u16_acc;
let result = if usecharidx {
let n = units.len() as varnumber_T;
match idx {
i if i == n => total_u16,
i if i > n => -1,
i => units[i as usize].1,
}
} else {
let strlen = s.len() as varnumber_T;
match idx {
i if i == strlen => total_u16,
i if i > strlen => -1,
i => units
.iter()
.take_while(|(sb, _)| (*sb as varnumber_T) <= i)
.last()
.map_or(0, |(_, before)| *before),
}
};
rettv.vval = v_number(result);
}