git.delta.rocks / jrsonnet / refs/commits / b5d51b90d7ba

difftreelog

source

crates/jrsonnet-stdlib/src/strings.rs6.7 KiBsourcehistory
1use std::collections::BTreeSet;23use jrsonnet_evaluator::{4	bail,5	error::{ErrorKind::*, Result},6	function::builtin,7	typed::{Either2, Typed, M1},8	val::{ArrValue, IndexableVal},9	Either, IStr, Val,10};1112#[builtin]13pub const fn builtin_codepoint(str: char) -> u32 {14	str as u3215}1617#[builtin]18pub fn builtin_substr(str: IStr, from: usize, len: usize) -> String {19	str.chars().skip(from).take(len).collect()20}2122#[builtin]23pub fn builtin_char(n: u32) -> Result<char> {24	Ok(std::char::from_u32(n).ok_or_else(|| InvalidUnicodeCodepointGot(n))?)25}2627#[builtin]28pub fn builtin_str_replace(str: String, from: IStr, to: IStr) -> String {29	str.replace(&from as &str, &to as &str)30}3132#[builtin]33pub fn builtin_escape_string_bash(str_: String) -> String {34	const QUOTE: char = '\'';35	let mut out = str_.replace(QUOTE, "'\"'\"'");36	out.insert(0, QUOTE);37	out.push(QUOTE);38	out39}4041#[builtin]42pub fn builtin_escape_string_dollars(str_: String) -> String {43	str_.replace('$', "$$")44}4546#[builtin]47pub fn builtin_is_empty(str: String) -> bool {48	str.is_empty()49}5051#[builtin]52pub fn builtin_equals_ignore_case(str1: String, str2: String) -> bool {53	str1.to_ascii_lowercase() == str2.to_ascii_lowercase()54}5556#[builtin]57pub fn builtin_splitlimit(str: IStr, c: IStr, maxsplits: Either![usize, M1]) -> ArrValue {58	use Either2::*;59	match maxsplits {60		A(n) => str.splitn(n + 1, &c as &str).map(Val::string).collect(),61		B(_) => str.split(&c as &str).map(Val::string).collect(),62	}63}6465#[builtin]66pub fn builtin_splitlimitr(str: IStr, c: IStr, maxsplits: Either![usize, M1]) -> ArrValue {67	use Either2::*;68	match maxsplits {69		A(n) =>70		// rsplitn does not implement DoubleEndedIterator so collect into71		// a temporary vec72		{73			str.rsplitn(n + 1, &c as &str)74				.map(Val::string)75				.collect::<Vec<_>>()76				.into_iter()77				.rev()78				.collect()79		}80		B(_) => str.split(&c as &str).map(Val::string).collect(),81	}82}8384#[builtin]85pub fn builtin_split(str: IStr, c: IStr) -> ArrValue {86	use Either2::*;87	builtin_splitlimit(str, c, B(M1))88}8990#[builtin]91pub fn builtin_ascii_upper(str: IStr) -> String {92	str.to_ascii_uppercase()93}9495#[builtin]96pub fn builtin_ascii_lower(str: IStr) -> String {97	str.to_ascii_lowercase()98}99100#[builtin]101pub fn builtin_find_substr(pat: IStr, str: IStr) -> ArrValue {102	if pat.is_empty() || str.is_empty() || pat.len() > str.len() {103		return ArrValue::empty();104	}105106	let str = str.as_str();107	let pat = pat.as_bytes();108	let strb = str.as_bytes();109110	let max_pos = str.len() - pat.len();111112	let mut out: Vec<Val> = Vec::new();113	for (ch_idx, (i, _)) in str114		.char_indices()115		.take_while(|(i, _)| i <= &max_pos)116		.enumerate()117	{118		if &strb[i..i + pat.len()] == pat {119			out.push(Val::Num(ch_idx as f64));120		}121	}122	out.into()123}124125#[builtin]126pub fn builtin_parse_int(str: IStr) -> Result<f64> {127	if let Some(raw) = str.strip_prefix('-') {128		if raw.is_empty() {129			bail!("integer only consists of a minus")130		}131132		parse_nat::<10>(raw).map(|value| -value)133	} else {134		if str.is_empty() {135			bail!("empty integer")136		}137138		parse_nat::<10>(str.as_str())139	}140}141142#[builtin]143pub fn builtin_parse_octal(str: IStr) -> Result<f64> {144	if str.is_empty() {145		bail!("empty octal integer");146	}147148	parse_nat::<8>(str.as_str())149}150151#[builtin]152pub fn builtin_parse_hex(str: IStr) -> Result<f64> {153	if str.is_empty() {154		bail!("empty hexadecimal integer");155	}156157	parse_nat::<16>(str.as_str())158}159160fn parse_nat<const BASE: u32>(raw: &str) -> Result<f64> {161	const ZERO_CODE: u32 = '0' as u32;162	const UPPER_A_CODE: u32 = 'A' as u32;163	const LOWER_A_CODE: u32 = 'a' as u32;164165	#[inline]166	fn checked_sub_if(condition: bool, lhs: u32, rhs: u32) -> Option<u32> {167		if condition {168			lhs.checked_sub(rhs)169		} else {170			None171		}172	}173174	debug_assert!(175		1 <= BASE && BASE <= 16,176		"integer base should be between 1 and 16"177	);178179	let base = f64::from(BASE);180181	raw.chars().try_fold(0f64, |aggregate, digit| {182		let digit = digit as u32;183		// if-let-else looks better here than Option combinators184		#[allow(clippy::option_if_let_else)]185		let digit = if let Some(digit) = checked_sub_if(BASE > 10, digit, LOWER_A_CODE) {186			digit + 10187		} else if let Some(digit) = checked_sub_if(BASE > 10, digit, UPPER_A_CODE) {188			digit + 10189		} else {190			digit.checked_sub(ZERO_CODE).unwrap_or(BASE)191		};192193		if digit < BASE {194			Ok(base.mul_add(aggregate, f64::from(digit)))195		} else {196			bail!("{raw:?} is not a base {BASE} integer");197		}198	})199}200201#[cfg(feature = "exp-bigint")]202#[builtin]203pub fn builtin_bigint(v: Either![f64, IStr]) -> Result<Val> {204	use jrsonnet_evaluator::runtime_error;205	use Either2::*;206	Ok(match v {207		A(a) => {208			Val::BigInt(Box::new(a.to_string().parse().map_err(|e| {209				runtime_error!("number is not convertible to bigint: {e}")210			})?))211		}212		B(b) => Val::BigInt(Box::new(213			b.as_str()214				.parse()215				.map_err(|e| runtime_error!("bad bigint: {e}"))?,216		)),217	})218}219220#[builtin]221pub fn builtin_string_chars(str: IStr) -> ArrValue {222	ArrValue::chars(str.chars())223}224225#[builtin]226pub fn builtin_lstrip_chars(str: IStr, chars: IndexableVal) -> Result<IStr> {227	if str.is_empty() || chars.is_empty() {228		return Ok(str);229	}230231	let pattern = new_trim_pattern(chars)?;232	Ok(str.as_str().trim_start_matches(pattern).into())233}234235#[builtin]236pub fn builtin_rstrip_chars(str: IStr, chars: IndexableVal) -> Result<IStr> {237	if str.is_empty() || chars.is_empty() {238		return Ok(str);239	}240241	let pattern = new_trim_pattern(chars)?;242	Ok(str.as_str().trim_end_matches(pattern).into())243}244245#[builtin]246pub fn builtin_strip_chars(str: IStr, chars: IndexableVal) -> Result<IStr> {247	if str.is_empty() || chars.is_empty() {248		return Ok(str);249	}250251	let pattern = new_trim_pattern(chars)?;252	Ok(str.as_str().trim_matches(pattern).into())253}254255fn new_trim_pattern(chars: IndexableVal) -> Result<impl Fn(char) -> bool> {256	let chars: BTreeSet<char> = match chars {257		IndexableVal::Str(chars) => chars.chars().collect(),258		IndexableVal::Arr(chars) => chars259			.iter()260			.filter_map(|it| it.map(|it| char::from_untyped(it).ok()).transpose())261			.collect::<Result<_, _>>()?,262	};263264	Ok(move |char| chars.contains(&char))265}266267#[cfg(test)]268mod tests {269	use super::*;270271	#[test]272	fn parse_nat_base_8() {273		assert_eq!(parse_nat::<8>("0").unwrap(), 0.);274		assert_eq!(parse_nat::<8>("5").unwrap(), 5.);275		assert_eq!(parse_nat::<8>("32").unwrap(), 0o32 as f64);276		assert_eq!(parse_nat::<8>("761").unwrap(), 0o761 as f64);277	}278279	#[test]280	fn parse_nat_base_10() {281		assert_eq!(parse_nat::<10>("0").unwrap(), 0.);282		assert_eq!(parse_nat::<10>("3").unwrap(), 3.);283		assert_eq!(parse_nat::<10>("27").unwrap(), 27.);284		assert_eq!(parse_nat::<10>("123").unwrap(), 123.);285	}286287	#[test]288	fn parse_nat_base_16() {289		assert_eq!(parse_nat::<16>("0").unwrap(), 0.);290		assert_eq!(parse_nat::<16>("A").unwrap(), 10.);291		assert_eq!(parse_nat::<16>("a9").unwrap(), 0xA9 as f64);292		assert_eq!(parse_nat::<16>("BbC").unwrap(), 0xBBC as f64);293	}294}