use super::value::compare_bytes;
use super::{QueryError, QueryValue, Truth, ValueContext};
use std::cmp::Ordering;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum StringPredicate {
StartsWith,
EndsWith,
Contains,
}
fn truncated_integer(value: f64) -> Option<i64> {
let value = value.trunc();
(value.is_finite()
&& (-9_223_372_036_854_775_808.0..9_223_372_036_854_775_808.0).contains(&value))
.then_some(value as i64)
}
fn decimal_integer(text: &str) -> Result<Option<i64>, QueryError> {
let text = text.trim();
let (negative, text) = if let Some(text) = text.strip_prefix('-') {
(true, text)
} else {
(false, text.strip_prefix('+').unwrap_or(text))
};
let (mantissa, exponent) = match text.split_once(['e', 'E']) {
Some((mantissa, exponent)) => {
let Ok(exponent) = exponent.parse::<i32>() else {
return Ok(None);
};
(mantissa, i64::from(exponent))
}
None => (text, 0),
};
let (whole, fraction) = mantissa.split_once('.').unwrap_or((mantissa, ""));
if (whole.is_empty() && fraction.is_empty())
|| !whole
.bytes()
.chain(fraction.bytes())
.all(|byte| byte.is_ascii_digit())
{
return Ok(None);
}
let point = i64::try_from(whole.len()).map_err(|_| QueryError::ValueTooLarge)? + exponent;
if point <= 0 {
return Ok(Some(0));
}
let mut value = 0_i64;
let mut consumed = 0_i64;
for byte in whole
.bytes()
.chain(fraction.bytes())
.take(usize::try_from(point).map_err(|_| QueryError::ArithmeticOverflow)?)
{
value = value
.checked_mul(10)
.and_then(|value| value.checked_sub(i64::from(byte - b'0')))
.ok_or(QueryError::ArithmeticOverflow)?;
consumed += 1;
}
if value != 0 {
if point - consumed > 18 {
return Err(QueryError::ArithmeticOverflow);
}
for _ in consumed..point {
value = value
.checked_mul(10)
.ok_or(QueryError::ArithmeticOverflow)?;
}
}
Ok(Some(if negative {
value
} else {
value.checked_neg().ok_or(QueryError::ArithmeticOverflow)?
}))
}
fn charge_numeric_text(text: &str, context: &mut ValueContext<'_>) -> Result<(), QueryError> {
for _ in text.bytes() {
context.step()?;
}
Ok(())
}
impl QueryValue<'_> {
pub fn to_integer(self, context: &mut ValueContext<'_>) -> Result<Self, QueryError> {
context.step()?;
self.validate(context)?;
Ok(match self {
Self::Null | Self::I64(_) => self,
Self::F64(value) => {
Self::I64(truncated_integer(value).ok_or(QueryError::ArithmeticOverflow)?)
}
Self::String(text) => {
charge_numeric_text(text, context)?;
decimal_integer(text)?.map_or(Self::Null, Self::I64)
}
_ => return Err(QueryError::Type),
})
}
pub fn to_float(self, context: &mut ValueContext<'_>) -> Result<Self, QueryError> {
context.step()?;
self.validate(context)?;
Ok(match self {
Self::Null | Self::F64(_) => self,
Self::I64(value) => Self::F64(value as f64),
Self::String(text) => {
charge_numeric_text(text, context)?;
match text.trim().parse::<f64>() {
Ok(value) => Self::F64(value).positive()?,
Err(_) => Self::Null,
}
}
_ => return Err(QueryError::Type),
})
}
pub fn floor(self, context: &mut ValueContext<'_>) -> Result<Self, QueryError> {
context.step()?;
self.validate(context)?;
Ok(match self.positive()? {
Self::Null => Self::Null,
Self::I64(value) => Self::F64(value as f64),
Self::F64(value) => Self::F64(value.floor()),
_ => return Err(QueryError::Type),
})
}
pub fn round(self, context: &mut ValueContext<'_>) -> Result<Self, QueryError> {
context.step()?;
self.validate(context)?;
Ok(match self.positive()? {
Self::Null => Self::Null,
Self::I64(value) => Self::F64(value as f64),
Self::F64(value) => {
let floor = value.floor();
Self::F64(if value - floor >= 0.5 {
floor + 1.0
} else {
floor
})
}
_ => return Err(QueryError::Type),
})
}
pub fn abs(self, context: &mut ValueContext<'_>) -> Result<Self, QueryError> {
context.step()?;
self.validate(context)?;
Ok(match self.positive()? {
Self::Null => Self::Null,
Self::I64(value) => {
Self::I64(value.checked_abs().ok_or(QueryError::ArithmeticOverflow)?)
}
Self::F64(value) => Self::F64(value.abs()),
_ => return Err(QueryError::Type),
})
}
pub fn size(self, context: &mut ValueContext<'_>) -> Result<Self, QueryError> {
context.step()?;
self.validate(context)?;
let length = match self {
Self::Null => return Ok(Self::Null),
Self::List(list) => list.len(),
Self::String(text) => {
let mut count = 0_usize;
for _ in text.chars() {
context.step()?;
count += 1;
}
count
}
_ => return Err(QueryError::Type),
};
Ok(Self::I64(
i64::try_from(length).map_err(|_| QueryError::WorkLimit)?,
))
}
pub fn index(self, index: Self, context: &mut ValueContext<'_>) -> Result<Self, QueryError> {
context.step()?;
self.validate(context)?;
index.validate(context)?;
if matches!(self, Self::Null) || matches!(index, Self::Null) {
return Ok(Self::Null);
}
let (Self::List(list), Self::I64(index)) = (self, index) else {
return Err(QueryError::Type);
};
let length = i128::try_from(list.len()).map_err(|_| QueryError::ListLimit)?;
let position = if index < 0 {
length + i128::from(index)
} else {
i128::from(index)
};
if position < 0 || position >= length {
return Ok(Self::Null);
}
list.get(usize::try_from(position).map_err(|_| QueryError::ListLimit)?)
.ok_or(QueryError::ListLimit)
}
pub fn string_predicate(
self,
pattern: Self,
operation: StringPredicate,
context: &mut ValueContext<'_>,
) -> Result<Truth, QueryError> {
context.step()?;
self.validate(context)?;
pattern.validate(context)?;
if matches!(self, Self::Null) || matches!(pattern, Self::Null) {
return Ok(Truth::Unknown);
}
let (Self::String(text), Self::String(pattern)) = (self, pattern) else {
return Err(QueryError::Type);
};
let text = text.as_bytes();
let pattern = pattern.as_bytes();
let matched = match operation {
StringPredicate::StartsWith => match text.get(..pattern.len()) {
Some(prefix) => compare_bytes(prefix, pattern, context)? == Ordering::Equal,
None => false,
},
StringPredicate::EndsWith => match text
.len()
.checked_sub(pattern.len())
.and_then(|start| text.get(start..))
{
Some(suffix) => compare_bytes(suffix, pattern, context)? == Ordering::Equal,
None => false,
},
StringPredicate::Contains if pattern.is_empty() => true,
StringPredicate::Contains => {
let mut found = false;
for window in text.windows(pattern.len()) {
context.step()?;
if compare_bytes(window, pattern, context)? == Ordering::Equal {
found = true;
break;
}
}
found
}
};
Ok(if matched { Truth::True } else { Truth::False })
}
}