use std::{
fmt,
hash::{Hash, Hasher},
};
use crate::{error::LuaErrorSrc, util::SliceExt, LuaError, Result};
pub type LuaInt = i64;
pub type LuaFloat = f64;
#[derive(Clone, Debug)]
pub enum Numeric {
Integer(LuaInt),
Float(LuaFloat),
}
impl Numeric {
pub fn from_str(str: &[u8]) -> Result<Self> {
Self::from_str_radix(str, None)
}
pub fn from_str_radix(str: &[u8], base: Option<u32>) -> Result<Self> {
let str = str.trim();
Ok(match base {
Some(base) => {
Numeric::Integer(i64::from_str_radix(&String::from_utf8_lossy(str), base)?)
}
None => {
let hex = if !str.is_empty() && matches!(&str[0..1], b"+" | b"-") {
str.len() >= 3 && matches!(&str[1..3], b"0x" | b"0X")
} else {
str.len() >= 2 && matches!(&str[0..2], b"0x" | b"0X")
};
if hex {
match from_str_radix_wrapping(str, 16) {
Some(n) => Numeric::Integer(n),
None => match parse_f64_hex(str) {
Some(f) => Numeric::Float(f),
None => {
return err!(LuaError::CustomString(
"invalid hex float".to_string()
))
}
},
}
} else {
match String::from_utf8_lossy(str).parse() {
Ok(n) => Numeric::Integer(n),
Err(..) => Numeric::Float(parse_f64_dec(str)?),
}
}
}
})
}
pub(crate) fn from_usize_try_int(u: usize) -> Numeric {
if u <= i64::MAX as usize {
Numeric::Integer(u as i64)
} else {
Numeric::Float(u as f64)
}
}
pub(crate) fn from_f64_try_int(f: f64) -> Numeric {
if f > i128::MIN as f64
&& f < i128::MAX as f64
&& f as i128 >= i64::MIN as i128
&& f as i128 <= i64::MAX as i128
{
Numeric::Integer(f as i64)
} else {
Numeric::Float(f)
}
}
pub(crate) fn to_int(&self) -> Result<i64> {
match self {
Numeric::Integer(n) => Ok(*n),
Numeric::Float(..) => err!(LuaError::FloatToInt(LuaErrorSrc::None)),
}
}
pub(crate) fn coerce_int(&self) -> Result<i64> {
match self {
Numeric::Integer(n) => Ok(*n),
Numeric::Float(n) => {
if n.fract().abs() <= f64::EPSILON
&& *n > i128::MIN as f64
&& *n < i128::MAX as f64
&& *n as i128 >= i64::MIN as i128
&& *n as i128 <= i64::MAX as i128
{
Ok(*n as i64)
} else {
err!(LuaError::FloatToInt(LuaErrorSrc::None))
}
}
}
}
pub(crate) fn to_float(&self) -> f64 {
match self {
Numeric::Integer(n) => *n as f64,
Numeric::Float(n) => *n,
}
}
pub(super) fn coerce_int_failover(&self) -> Numeric {
match self {
Numeric::Integer(n) => Numeric::Integer(*n),
Numeric::Float(n) => {
if n.fract().abs() <= f64::EPSILON {
Numeric::Integer(*n as i64)
} else {
Numeric::Float(*n)
}
}
}
}
}
impl fmt::Display for Numeric {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Numeric::Integer(n) => write!(f, "{}", n),
Numeric::Float(n) => write!(f, "{:?}", n),
}
}
}
impl Hash for Numeric {
fn hash<H: Hasher>(&self, state: &mut H) {
match self {
Numeric::Integer(n) => {
state.write_u8(0);
state.write_i64(*n);
}
Numeric::Float(f) => {
state.write_u8(1);
for byte in f.to_be_bytes() {
state.write_u8(byte);
}
}
}
}
}
impl PartialEq for Numeric {
fn eq(&self, other: &Self) -> bool {
match (self.coerce_int(), other.coerce_int()) {
(Ok(n1), Ok(n2)) => n1 == n2,
(Ok(..), ..) | (.., Ok(..)) => false,
_ => {
let n1 = self.to_float();
let n2 = other.to_float();
if n1.is_finite() && n2.is_finite() {
(n1 - n2).abs() < f64::EPSILON
} else {
n1 == n2
}
}
}
}
}
impl Eq for Numeric {}
pub fn from_str_radix_wrapping(str: &[u8], radix: u32) -> Option<i64> {
assert!((2..=36).contains(&radix), "radix must be in range [2, 36]");
let mut out: i64 = 0;
let (positive, digits) = match str[0] {
b'-' => (false, &str[1..]),
b'+' => (true, &str[1..]),
_ => (true, str),
};
let digits = if digits.len() >= 2 && matches!(&digits[0..2], b"0x" | b"0X") {
&digits[2..]
} else {
digits
};
if digits.is_empty() {
return None;
}
for &d in digits {
let d = match (d as char).to_digit(radix) {
Some(d) => d,
None => return None,
};
out = out.wrapping_mul(radix as i64);
out = if positive {
out.wrapping_add(d as i64)
} else {
out.wrapping_sub(d as i64)
};
}
Some(out)
}
pub fn parse_f64_dec(str: &[u8]) -> Result<f64> {
let f: f64 = String::from_utf8_lossy(str).parse()?;
if f.is_nan() || f.is_infinite() {
err!(LuaError::CustomString(
"inf and nan are invalid numeric literals".to_string()
))
} else {
Ok(f)
}
}
const MAX_SIG_DIG: usize = 30;
pub fn parse_f64_hex(str: &[u8]) -> Option<f64> {
let str = str.trim();
let mut acc: f64 = 0.0; let mut sig_dig = 0; let mut non_sig_dig = 0; let mut exp: i64 = 0; let mut has_dot = false;
let (str, neg) = match str[0] {
b'-' => (&str[1..], true),
b'+' => (&str[1..], false),
_ => (str, false),
};
if !matches!(&str[..2], b"0x" | b"0X") {
return None;
}
let mut str = str[2..].iter().peekable();
while let Some(b) = str.peek() {
if matches!(b, b'.') {
if has_dot {
return None;
}
has_dot = true;
} else if let Some(d) = (**b as char).to_digit(16) {
if sig_dig == 0 && d == 0 {
non_sig_dig += 1;
} else if sig_dig < MAX_SIG_DIG {
sig_dig += 1;
acc = acc.mul_add(16.0, if neg { -(d as f64) } else { d as f64 });
} else {
exp += 4; }
if has_dot {
exp -= 4; }
} else {
break;
}
str.next();
}
if non_sig_dig + sig_dig == 0 {
return None;
}
if matches!(str.peek(), Some(b'p' | b'P')) {
str.next();
let mut exp_lit: i64 = 0;
let exp_neg = match str.peek() {
Some(b'-') => {
str.next();
true
}
Some(b'+') => {
str.next();
false
}
_ => false,
};
str.peek()?;
for b in &mut str {
match (*b as char).to_digit(10) {
Some(d) => {
exp_lit *= 10;
if exp_neg {
exp_lit -= d as i64;
} else {
exp_lit += d as i64;
}
}
None => return None,
}
}
exp += exp_lit;
}
if str.next().is_some() {
return None;
}
Some(acc * (exp as f64).exp2())
}