diff --git a/crates/jrsonnet-stdlib/src/lib.rs b/crates/jrsonnet-stdlib/src/lib.rs index 103fe07b..dc385427 100644 --- a/crates/jrsonnet-stdlib/src/lib.rs +++ b/crates/jrsonnet-stdlib/src/lib.rs @@ -128,6 +128,9 @@ pub fn stdlib_uncached(settings: Rc>) -> ObjValue { ("asciiUpper", builtin_ascii_upper::INST), ("asciiLower", builtin_ascii_lower::INST), ("findSubstr", builtin_find_substr::INST), + ("parseInt", builtin_parse_int::INST), + ("parseOctal", builtin_parse_octal::INST), + ("parseHex", builtin_parse_hex::INST), // Misc ("length", builtin_length::INST), ("startsWith", builtin_starts_with::INST), @@ -312,7 +315,7 @@ impl jrsonnet_evaluator::ContextInitializer for ContextInitializer { out.build() } #[cfg(feature = "legacy-this-file")] - fn initialize(&self, s: State, source: Source) -> jrsonnet_evaluator::Context { + fn initialize(&self, s: State, source: Source) -> Context { let mut builder = ObjValueBuilder::new(); builder.with_super(self.stdlib_obj.clone()); builder diff --git a/crates/jrsonnet-stdlib/src/strings.rs b/crates/jrsonnet-stdlib/src/strings.rs index 2d72a95a..fe070df5 100644 --- a/crates/jrsonnet-stdlib/src/strings.rs +++ b/crates/jrsonnet-stdlib/src/strings.rs @@ -1,6 +1,7 @@ use jrsonnet_evaluator::{ error::{ErrorKind::*, Result}, function::builtin, + throw, typed::{Either2, VecVal, M1}, val::ArrValue, Either, IStr, Val, @@ -73,3 +74,106 @@ pub fn builtin_find_substr(pat: IStr, str: IStr) -> Result { } Ok(out.into()) } + +#[builtin] +pub fn builtin_parse_int(raw: IStr) -> Result { + if let Some(raw) = raw.strip_prefix('-') { + if raw.is_empty() { + throw!("integer only consists of a minus") + } + + parse_nat::<10>(raw).map(|value| -value) + } else { + if raw.is_empty() { + throw!("empty integer") + } + + parse_nat::<10>(raw.as_str()) + } +} + +#[builtin] +pub fn builtin_parse_octal(raw: IStr) -> Result { + if raw.is_empty() { + throw!("empty octal integer"); + } + + parse_nat::<8>(raw.as_str()) +} + +#[builtin] +pub fn builtin_parse_hex(raw: IStr) -> Result { + if raw.is_empty() { + throw!("empty hexadecimal integer"); + } + + parse_nat::<16>(raw.as_str()) +} + +fn parse_nat(raw: &str) -> Result { + debug_assert!( + 1 <= BASE && BASE <= 16, + "integer base should be between 1 and 16" + ); + + const ZERO_CODE: u32 = '0' as u32; + const UPPER_A_CODE: u32 = 'A' as u32; + const LOWER_A_CODE: u32 = 'a' as u32; + + #[inline] + fn checked_sub_if(condition: bool, lhs: u32, rhs: u32) -> Option { + if condition { + lhs.checked_sub(rhs) + } else { + None + } + } + + let base = BASE as f64; + + raw.chars().try_fold(0f64, |aggregate, digit| { + let digit = digit as u32; + let digit = if let Some(digit) = checked_sub_if(BASE > 10, digit, LOWER_A_CODE) { + digit + 10 + } else if let Some(digit) = checked_sub_if(BASE > 10, digit, UPPER_A_CODE) { + digit + 10 + } else { + digit.checked_sub(ZERO_CODE).unwrap_or(BASE) + }; + + if digit < BASE { + Ok(base * aggregate + digit as f64) + } else { + throw!("{raw:?} is not a base {BASE} integer",); + } + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn parse_nat_base_8() { + assert_eq!(parse_nat::<8>("0").unwrap(), 0.); + assert_eq!(parse_nat::<8>("5").unwrap(), 5.); + assert_eq!(parse_nat::<8>("32").unwrap(), 0o32 as f64); + assert_eq!(parse_nat::<8>("761").unwrap(), 0o761 as f64); + } + + #[test] + fn parse_nat_base_10() { + assert_eq!(parse_nat::<10>("0").unwrap(), 0.); + assert_eq!(parse_nat::<10>("3").unwrap(), 3.); + assert_eq!(parse_nat::<10>("27").unwrap(), 27.); + assert_eq!(parse_nat::<10>("123").unwrap(), 123.); + } + + #[test] + fn parse_nat_base_16() { + assert_eq!(parse_nat::<16>("0").unwrap(), 0.); + assert_eq!(parse_nat::<16>("A").unwrap(), 10.); + assert_eq!(parse_nat::<16>("a9").unwrap(), 0xA9 as f64); + assert_eq!(parse_nat::<16>("BbC").unwrap(), 0xBBC as f64); + } +}