use std::cmp::Ordering;
pub(crate) fn evaluate(
expression: &str,
substitute: &mut dyn FnMut(&str) -> String,
) -> Option<bool> {
let expression = expression.trim_matches(|c: char| c.is_ascii_whitespace());
for (index, _) in expression.char_indices().skip(1) {
let rest = &expression[index..];
let Some(operator) = ["==", "!=", "<=", ">=", "<", ">"]
.into_iter()
.find(|op| rest.starts_with(op) && rest.len() > op.len())
else {
continue;
};
let right = &rest[operator.len()..];
let left = Value::resolve(expression[..index].trim_end_matches(' '), substitute);
let right = Value::resolve(right.trim_start_matches(' '), substitute);
let order = left.compare(&right);
let equal = match (&left, &right) {
(Value::Nil, Value::Nil) => true,
(Value::Bool(a), Value::Bool(b)) => a == b,
_ => order == Some(Ordering::Equal),
};
return Some(match operator {
"==" => equal,
"!=" => !equal,
"<" => order == Some(Ordering::Less),
">" => order == Some(Ordering::Greater),
"<=" => matches!(order, Some(Ordering::Less | Ordering::Equal)),
">=" => matches!(order, Some(Ordering::Greater | Ordering::Equal)),
_ => unreachable!(),
});
}
None
}
enum Value {
Nil,
Bool(bool),
String(String),
Integer(String),
Float(f64),
}
impl Value {
fn resolve(raw: &str, substitute: &mut dyn FnMut(&str) -> String) -> Self {
let quoted = (raw.starts_with('"') && raw.ends_with('"'))
|| (raw.starts_with('\'') && raw.ends_with('\''));
let value = if quoted { &raw[1..] } else { raw };
let value = if value.contains('{') {
substitute(value)
} else {
value.to_owned()
};
if quoted {
Self::String(value)
} else {
match value.as_str() {
"" => Self::Nil,
"true" => Self::Bool(true),
"false" => Self::Bool(false),
s if s
.trim_end_matches(|c: char| c.is_ascii_whitespace())
.is_empty() =>
{
Self::String(" ".into())
}
s if s.contains('.') => Self::Float(float_prefix(s)),
s => Self::Integer(integer_prefix(s)),
}
}
}
fn compare(&self, other: &Self) -> Option<Ordering> {
match (self, other) {
(Self::String(a), Self::String(b)) => Some(a.cmp(b)),
(Self::Integer(a), Self::Integer(b)) => Some(compare_integers(a, b)),
(Self::Float(a), Self::Float(b)) => a.partial_cmp(b),
(Self::Integer(a), Self::Float(b)) => compare_integer_float(a, *b),
(Self::Float(a), Self::Integer(b)) => {
compare_integer_float(b, *a).map(Ordering::reverse)
}
_ => None,
}
}
}
fn integer_prefix(raw: &str) -> String {
let raw = raw.trim_start_matches(|c: char| c.is_ascii_whitespace());
let negative = raw.starts_with('-');
let raw = raw.strip_prefix(['+', '-']).unwrap_or(raw);
let end = digits_end(raw.as_bytes(), 0);
let digits = raw[..end].replace('_', "");
let digits = digits.trim_start_matches('0');
if digits.is_empty() {
"0".into()
} else if negative {
format!("-{digits}")
} else {
digits.into()
}
}
fn compare_integers(a: &str, b: &str) -> Ordering {
let (an, bn) = (a.starts_with('-'), b.starts_with('-'));
if an != bn {
return bn.cmp(&an);
}
let order = a.len().cmp(&b.len()).then_with(|| a.cmp(b));
if an {
order.reverse()
} else {
order
}
}
fn compare_integer_float(integer: &str, float: f64) -> Option<Ordering> {
if float.is_nan() {
return None;
}
if float.is_infinite() {
return Some(if float.is_sign_positive() {
Ordering::Less
} else {
Ordering::Greater
});
}
let whole = if float.abs() < 1.0 {
"0".into()
} else {
format!("{:.0}", float.trunc())
};
let order = compare_integers(integer, &whole);
Some(if order == Ordering::Equal {
0.0_f64.partial_cmp(&float.fract()).unwrap()
} else {
order
})
}
fn digits_end(bytes: &[u8], mut index: usize) -> usize {
let start = index;
while index < bytes.len() {
if bytes[index].is_ascii_digit() {
index += 1;
} else if bytes[index] == b'_'
&& index > start
&& bytes.get(index + 1).is_some_and(u8::is_ascii_digit)
{
index += 2;
} else {
break;
}
}
index
}
fn float_prefix(raw: &str) -> f64 {
let raw = raw.trim_start_matches(|c: char| c.is_ascii_whitespace());
let bytes = raw.as_bytes();
let start = usize::from(matches!(bytes.first(), Some(b'+' | b'-')));
let mut end = digits_end(bytes, start);
let mut has_digits = end > start;
if bytes.get(end) == Some(&b'.') {
let fraction = digits_end(bytes, end + 1);
if fraction == end + 1 {
return raw[..end].replace('_', "").parse().unwrap_or(0.0);
}
has_digits |= fraction > end + 1;
end = fraction;
}
if !has_digits {
return 0.0;
}
if matches!(bytes.get(end), Some(b'e' | b'E')) {
let exp_start = end + 1 + usize::from(matches!(bytes.get(end + 1), Some(b'+' | b'-')));
let exp_end = digits_end(bytes, exp_start);
if exp_end > exp_start {
end = exp_end;
}
}
raw[..end].replace('_', "").parse().unwrap_or(0.0)
}