git.delta.rocks / jrsonnet / refs/commits / 398f421f2a20

difftreelog

feat add intrinsics for numeric parsing

Petr Portnov2022-11-23parent: #15d127a.patch.diff
in: master

2 files changed

modifiedcrates/jrsonnet-stdlib/src/lib.rsdiffbeforeafterboth
128 ("asciiUpper", builtin_ascii_upper::INST),128 ("asciiUpper", builtin_ascii_upper::INST),
129 ("asciiLower", builtin_ascii_lower::INST),129 ("asciiLower", builtin_ascii_lower::INST),
130 ("findSubstr", builtin_find_substr::INST),130 ("findSubstr", builtin_find_substr::INST),
131 ("parseInt", builtin_parse_int::INST),
132 ("parseOctal", builtin_parse_octal::INST),
133 ("parseHex", builtin_parse_hex::INST),
131 // Misc134 // Misc
132 ("length", builtin_length::INST),135 ("length", builtin_length::INST),
133 ("startsWith", builtin_starts_with::INST),136 ("startsWith", builtin_starts_with::INST),
312 out.build()315 out.build()
313 }316 }
314 #[cfg(feature = "legacy-this-file")]317 #[cfg(feature = "legacy-this-file")]
315 fn initialize(&self, s: State, source: Source) -> jrsonnet_evaluator::Context {318 fn initialize(&self, s: State, source: Source) -> Context {
316 let mut builder = ObjValueBuilder::new();319 let mut builder = ObjValueBuilder::new();
317 builder.with_super(self.stdlib_obj.clone());320 builder.with_super(self.stdlib_obj.clone());
318 builder321 builder
modifiedcrates/jrsonnet-stdlib/src/strings.rsdiffbeforeafterboth
--- 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,91 @@
 	}
 	Ok(out.into())
 }
+
+#[builtin]
+pub fn builtin_parse_int(raw: IStr) -> Result<f64> {
+	let mut chars = raw.chars();
+	if let Some(first_char) = chars.next() {
+		if first_char == '-' {
+			let remaining = chars.as_str();
+			if remaining.is_empty() {
+				throw!("Not an integer: \"{}\"", raw);
+			}
+			parse_nat::<10>(remaining).map(|value| -value)
+		} else {
+			parse_nat::<10>(raw.as_str())
+		}
+	} else {
+		throw!("Not an integer: \"{}\"", raw);
+	}
+}
+
+#[builtin]
+pub fn builtin_parse_octal(raw: IStr) -> Result<f64> {
+	if raw.is_empty() {
+		throw!("Not an octal number: \"\"");
+	}
+
+	parse_nat::<8>(raw.as_str())
+}
+
+#[builtin]
+pub fn builtin_parse_hex(raw: IStr) -> Result<f64> {
+	if raw.is_empty() {
+		throw!("Not hexadecimal: \"\"");
+	}
+
+	parse_nat::<16>(raw.as_str())
+}
+
+fn parse_nat<const BASE: u32>(raw: &str) -> Result<f64> {
+	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<u32> {
+		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_10() {
+		assert_eq!(parse_nat::<10>("0").unwrap(), 0.);
+		assert_eq!(parse_nat::<10>("3").unwrap(), 3.);
+		assert_eq!(parse_nat::<10>("27").unwrap(), 10. * 2. + 7.);
+		assert_eq!(parse_nat::<10>("123").unwrap(), 10. * (10. * 1. + 2.) + 3.);
+	}
+}