use rudb_common::{Error, Result, Value};
const STEPPED_STRING: &str = "Slice with steps has not been implemented for string types, you can \
consider rewriting your query as follows:\n SELECT array_to_string((str_split(string, \
'')[begin:end:step], '');";
pub(crate) fn extract(target: &Value, index: &Value) -> Result<Value> {
let index = whole(index)?;
match target {
Value::Varchar(text) => {
let length = text.chars().count() as i128;
match at(length, index) {
None => Ok(Value::Varchar(String::new())),
Some(found) => {
let character = text
.chars()
.nth(found)
.map_or_else(String::new, |character| character.to_string());
Ok(Value::Varchar(character))
}
}
}
Value::List { values, .. } => match at(values.len() as i128, index) {
None => Ok(Value::Null),
Some(found) => Ok(values[found].clone()),
},
other => Err(Error::internal(format!("a subscript of a {}", other.logical_type()))),
}
}
pub(crate) fn slice(
target: &Value,
begin: &Value,
end: &Value,
step: Option<&Value>,
) -> Result<Value> {
let step = match step {
Some(written) => Some(whole(written)?),
None => None,
};
let (begin, end) = (whole(begin)?, whole(end)?);
match target {
Value::Varchar(text) => {
if step.is_some() {
return Err(Error::not_implemented(STEPPED_STRING));
}
let characters: Vec<char> = text.chars().collect();
let kept: String = indices(characters.len() as i128, begin, end, None)?
.map(|at| characters[at])
.collect();
Ok(Value::Varchar(kept))
}
Value::List { element, values } => {
let kept = indices(values.len() as i128, begin, end, step)?
.map(|at| values[at].clone())
.collect();
Ok(Value::List { element: element.clone(), values: kept })
}
other => Err(Error::internal(format!("a slice of a {}", other.logical_type()))),
}
}
fn whole(value: &Value) -> Result<i128> {
value
.as_i64()
.map(i128::from)
.ok_or_else(|| Error::internal(format!("a subscript by a {}", value.logical_type())))
}
fn at(length: i128, index: i128) -> Option<usize> {
let found = if index < 0 { length + index + 1 } else { index };
if found < 1 || found > length {
return None;
}
usize::try_from(found - 1).ok()
}
fn indices(
length: i128,
begin: i128,
end: i128,
step: Option<i128>,
) -> Result<impl Iterator<Item = usize>> {
let from_end = |bound: i128| if bound < 0 { length + bound + 1 } else { bound };
let (first, last) = (from_end(begin), from_end(end));
let (start, stop, stride) = match step {
None => (first.max(1), last.min(length), 1),
Some(0) => return Err(Error::invalid_input("Slice step cannot be zero")),
Some(step) if step > 0 => {
let start = if first < 1 { first + step * up(1 - first, step) } else { first };
(start, last.min(length), step)
}
Some(step) => {
let stride = -step;
let start =
if first > length { first - stride * up(first - length, stride) } else { first };
(start, last.max(1), step)
}
};
let mut at = start;
Ok(std::iter::from_fn(move || {
let done = if stride > 0 { at > stop } else { at < stop };
if done {
return None;
}
let found = usize::try_from(at - 1).ok();
at += stride;
found
}))
}
fn up(gap: i128, stride: i128) -> i128 {
(gap + stride - 1) / stride
}
#[cfg(test)]
mod tests {
use rudb_common::LogicalType;
use super::*;
fn list(values: &[i64]) -> Value {
Value::List {
element: LogicalType::BigInt,
values: values.iter().copied().map(Value::BigInt).collect(),
}
}
fn numbers(value: &Value) -> Vec<i64> {
match value {
Value::List { values, .. } => {
values.iter().map(|held| held.as_i64().unwrap()).collect()
}
other => panic!("not a list: {other}"),
}
}
fn text(value: &Value) -> String {
match value {
Value::Varchar(held) => held.clone(),
other => panic!("not a string: {other}"),
}
}
#[test]
fn an_index_counts_from_one_and_a_negative_one_counts_from_the_end() {
let held = list(&[1, 2, 3]);
let index = |at: i64| extract(&held, &Value::BigInt(at)).expect("extracts");
assert_eq!(index(2), Value::BigInt(2));
assert_eq!(index(-1), Value::BigInt(3));
assert_eq!(index(-3), Value::BigInt(1));
assert_eq!(index(0), Value::Null);
assert_eq!(index(4), Value::Null);
assert_eq!(index(-4), Value::Null);
let word = Value::Varchar("abcdef".to_owned());
let letter = |at: i64| text(&extract(&word, &Value::BigInt(at)).expect("extracts"));
assert_eq!(letter(2), "b");
assert_eq!(letter(-1), "f");
assert_eq!(letter(0), "");
assert_eq!(letter(9), "");
}
#[test]
fn a_string_is_indexed_by_character() {
let word = Value::Varchar("héllo".to_owned());
assert_eq!(text(&extract(&word, &Value::BigInt(2)).expect("extracts")), "é");
let sliced = slice(&word, &Value::BigInt(2), &Value::BigInt(3), None).expect("slices");
assert_eq!(text(&sliced), "él");
}
#[test]
fn a_range_clamps_at_the_begin_and_at_the_end_but_not_the_same_way() {
let held = list(&[1, 2, 3, 4, 5]);
let range = |from: i64, to: i64| {
numbers(&slice(&held, &Value::BigInt(from), &Value::BigInt(to), None).expect("slices"))
};
assert_eq!(range(2, 4), vec![2, 3, 4]);
assert_eq!(range(-3, -1), vec![3, 4, 5]);
assert_eq!(range(0, 2), vec![1, 2]);
assert_eq!(range(2, 99), vec![2, 3, 4, 5]);
assert_eq!(range(-99, 99), vec![1, 2, 3, 4, 5]);
assert!(range(4, 2).is_empty());
assert!(range(99, 99).is_empty());
assert!(range(-99, -98).is_empty());
assert_eq!(range(1, -1), vec![1, 2, 3, 4, 5]);
}
#[test]
fn a_step_walks_the_range_and_a_negative_one_walks_it_backwards() {
let held = list(&[1, 2, 3, 4, 5]);
let walk = |from: i64, to: i64, by: i64| {
let step = Value::BigInt(by);
let sliced = slice(&held, &Value::BigInt(from), &Value::BigInt(to), Some(&step));
numbers(&sliced.expect("slices"))
};
assert_eq!(walk(1, 5, 2), vec![1, 3, 5]);
assert_eq!(walk(2, 5, 2), vec![2, 4]);
assert_eq!(walk(5, 1, -1), vec![5, 4, 3, 2, 1]);
assert_eq!(walk(5, 1, -2), vec![5, 3, 1]);
assert_eq!(walk(-1, -3, -1), vec![5, 4, 3]);
assert_eq!(walk(-99, 99, 2), vec![1, 3, 5]);
assert_eq!(walk(99, 1, -1), vec![5, 4, 3, 2, 1]);
assert_eq!(walk(3, -99, -1), vec![3, 2, 1]);
assert!(walk(2, 4, -1).is_empty());
assert!(walk(99, 99, -1).is_empty());
assert!(walk(5, 2, 2).is_empty());
}
#[test]
fn a_step_of_zero_and_a_step_on_a_string_are_both_refused() {
let zero = Value::BigInt(0);
let held = list(&[1, 2, 3]);
let error = slice(&held, &Value::BigInt(1), &Value::BigInt(3), Some(&zero))
.expect_err("a step of zero is refused");
assert_eq!(error.message(), "Slice step cannot be zero");
let word = Value::Varchar("abcdef".to_owned());
let error = slice(&word, &Value::BigInt(1), &Value::BigInt(3), Some(&zero))
.expect_err("a step on a string is refused");
assert!(
error.message().starts_with("Slice with steps has not been implemented"),
"{error}"
);
}
}